<?php

// Namespace

namespace Loop;


/**
 * CObject is an abstract class which every model should extend to get instant support of load/save/delete from the database
 *
 * @package		Loop
 * @category	Core
 */

abstract class CObject
{
	// Properties
	
	protected $autocompleteProperties = null;
	protected $className = null;
	protected $classNameWithoutNamespace = null;
	protected $databaseDriver = null;
	protected $defaultDatabaseDriver = null;
	protected $engine = null;
	protected $extensionId = null;
	protected $featureVariables = null;
	protected $filtersNames = null;
	protected $idName = null;
	protected $isLocalized = null;
	protected $isNew = null;
	protected $namesOfAncesters = null;
	protected $objectName = null;
	protected $objectUri = null;
	protected $parentProperties = null;
	protected $parents = null;
	protected $propertiesNames = null;
	protected $propertyNamePrefix = null;
	protected $referenceName = null;
	protected $tableName = null; 	// The name of the table
	protected $tableName_L = null;
	protected $viewName = null;
	
	public $defaultPropertyIcon = null;
	public $defaultPropertyDescription = null;
	public $defaultPropertyName = null;
	public $features = null;	
	public $fileProperties = null;	
	public $filters = null;	
	public $indexes = null;	
	public $localizedProperties = null;	
	public $smartProperties = null;	
	public $staticProperties = null;
	
	
	/**
	 * @param	string	$className				The class name (e.g. CMyObject)
	 * @param	string	$extensionId			The ID of the extension (e.g. My-Application)
	 */
	
	public function __construct($hasCustomProperties = false)
	{
		//

		$this->className = get_class($this);


		//

		$classNameFragments = explode('\\', $this->className);

		$this->classNameWithoutNamespace = array_pop($classNameFragments);
		$this->extensionId = implode('-', $classNameFragments);


		// Transform the class name into an object name (without C, no first capital letter)
		
		$objectNameFragments = explode('_', $this->classNameWithoutNamespace);
		
		foreach ($objectNameFragments as &$objectNameFragment)
		{
			$objectNameFragment = lcfirst(substr($objectNameFragment, 1));
		}

		$this->objectName = implode('_', $objectNameFragments);

		
		// Set default value of member attributes
		
		$this->autocompleteProperties = array();
		$this->engine = ENGINE_INNODB;
		$this->features = array();
		$this->fileName = $this->classNameWithoutNamespace . '.php';
		$this->fileProperties = array();
		$this->filters = array();
		$this->filtersNames = array();
		$this->idName = $this->objectName . 'Id';
		$this->indexes = array();
		$this->localizedProperties = array();
		$this->namesOfAncesters = array();
		$this->parentProperties = array();
		$this->parents = array();
		$this->propertiesNames = array();
		$this->referenceName = $this->objectName . 'Reference';
		$this->rules = array();
		$this->smartProperties = array();
		$this->staticProperties = array();
		$this->defaultDatabaseDriver = db($this->extensionId);
		
		$this->setup();


		// Create core properties
		
		app()->getObjectFactory()->createCoreProperties($this);
		
		
		// By default an object is not localized
		
		$this->isLocalized = false;
		
		
		// We can now create child properties, features and filters
		
		$this->createProperties();
		$this->createFeatures();
		$this->createFilters();


		// Create custom properties, if enabled

		if ($hasCustomProperties === true)
		{
			app()->getObjectFactory()->createCustomProperties($this);
		}

		
		// Try to include strings for the model
		
		$pathToExtension = app()->getPathHelper()->convertExtensionIdToPath($this->extensionId);

		$pathToString = $pathToExtension . 'String/Model/C' . ucfirst($this->objectName) . '_' . pref('[Loop]/localization/languageIsoCode') . '.php';
		
		if (file_exists($pathToString) == true)
		{
			require_once($pathToString);
		}
	}
	
	
	/**
	 * Captures get/set accessors (e.g. getId())
	 *
	 * @param	string	$functionName		The name of the function called
	 * @param	array	$functionArguments	Arguments called by the function
	 *
	 * @return	mixed						Will return anything the target function decides to return
	 */
	
	public function __call($functionName, $functionArguments = null)
	{
		// Make sure we've got the right number of arguments
		
		$defaultFunctionArguments = array(null, null);
		$arguments = array_merge($functionArguments, $defaultFunctionArguments);
		
		
		// Let's see what kind of function we're trying to reach

		if (preg_match('/^get(.+)/', $functionName, $matches) === 1)
		{
			return $this->getPropertyValue(lcfirst($matches[1]), $arguments[0]);
		}
		else if (preg_match('/^set(.+)/', $functionName, $matches) === 1)
		{
			return $this->setPropertyValue(lcfirst($matches[1]), $arguments[0], $arguments[1]);
		}
		else
		{
			// Maybe this function exists in one of the features

			foreach ($this->features as $extensionId => $features)
			{
				foreach ($features as $featureName)
				{
					try
					{
						// Get the feature

						try
						{
							$feature = feat($extensionId, $featureName);
						}
						catch (\Exception $e)
						{
							if ($e->getCode() != EXCEPTION_FEATURE_NOT_FOUND)
							{
								throw $e;
							}
							else
							{
								continue;
							}
						}

						
						// Skip the feature if the function dosn't exist

						if (is_callable(array($feature, $functionName)) == false)
						{
							continue;
						}

						
						// If no argument given, simply call the function
						// Otherwise, we need to use __call to pass arguments

						if (count($functionArguments) == 0)
						{
							return $feature->$functionName($this);
						}
						else
						{
							array_unshift($functionArguments, $this);
							return $feature->__call($functionName, $functionArguments);
						}
					}
					catch (\Exception $e)
					{
						if ($e->getCode() != EXCEPTION_OBJECT_FUNCTION_NOT_FOUND)
						{
							throw $e;
						}
					}
				}
			}


			// Then try on ancesters

			try
			{
				foreach ($this->namesOfAncesters as $ancesterName)
				{
					// Call the method on the ancester
					
					$ancester = app()->getObjectFactory()->convertPropertyToObject($this, $ancesterName);
					return $ancester->__call($functionName, $functionArguments);
				}
			}
			catch (\Exception $e)
			{
				if ($e->getCode() != EXCEPTION_OBJECT_FUNCTION_NOT_FOUND)
				{
					throw $e;
				}
			}
		}

			
		// The function does not exist!
		
		e
		(
			EXCEPTION_OBJECT_FUNCTION_NOT_FOUND,
			array
			(
				'className' => $this->className,
				'functionName' => $functionName
			)
		);
	}
	

	/**
	 *
	 */

	public function __get($propertyName)
	{
		if ($propertyName === 'databaseName')
		{
			$extensionId = $this->extensionId;

			if (pref('[' . $extensionId . ']/database/isShared') === true)
			{
				$extensionId = EXTENSION_APPLICATION;
			}

			return pref('[' . $extensionId . ']/database/name');
		}


		return $this->$propertyName;
	}
	

	/**
	 * Add a new variable on featureVariable array.
	 */

	public function addFeatureVariable($extensionId, $featureName, $variableName, $variableValue)
	{
		if (is_array($this->featureVariables) == false)
		{
			$this->featureVariables = array();
		}
		
		$this->featureVariables[$extensionId][$featureName][$variableName] = $variableValue;
	}

	
	/**
	 *
	 */
	
	public function api_autocomplete()
	{
		// Get API arguments
		
		$value = fv('value', true);
		$conditions = fv('conditions', false);
		
		
		// Call the method

		$result = $this->autocomplete($value, $conditions);
		
		
		return $result;
	}

	
	/**
	 *
	 */
	
	public function api_delete()
	{
		// Get API arguments
		
		$objectId = fv('objectId', true);
		$objectReference = fv('objectReference', false);
		
		
		// Load the object
		
		$this->loadSecure($objectId, $objectReference);
		
		
		// Prepare result before the delete
		
		$result = array
		(
			'urlOfRedirection' => $this->getUrlOfPage(ACTION_LIST)
		);
		
		
		// Call the method
		
		$this->delete();
		
		
		return $result;
	}


	/**
	 *
	 */
	
	public function api_deleteFile()
	{
		// Get API arguments

		$index = fv('index', true);
		$objectId = fv('objectId', true);
		$objectReference = fv('objectReference', false);
		$propertyName = fv('propertyName', true);

		
		// Load the object

		$this->loadSecure($objectId, $objectReference, null, array('acceptTemporaryObjects' => true));


		// Call the method

		$result = $this->deleteFile($propertyName, $index);


		return $result;
	}
	
	
	/**
	 *
	 */
	
	public function api_disable()
	{
		// Get API arguments
		
		$objectId = fv('objectId', true);
		$objectReference = fv('objectReference', false);
		
		
		// Load the object
		
		$this->loadSecure($objectId, $objectReference);
		
		
		// Call the method
		
		$this->disable();
	}
	
	
	/**
	 *
	 */
	
	public function api_enable()
	{
		// Get API arguments
		
		$objectId = fv('objectId', true);
		$objectReference = fv('objectReference', false);
		
		
		// Load the object
		
		$this->loadSecure($objectId, $objectReference, null, array('acceptDisabledObjects' => true));
		
		
		// Call the method
		
		$this->enable();
	}
	
	
	/**
	 *
	 */
	
	public function api_filter()
	{
		// Get API arguments
		
		$filters = fv('filters', false);
		$logicalOperator = fv('logicalOperator', false);
		$page = fv('page', false);
		
								
		// Call the method
		
		$result = $this->filter($filters, $logicalOperator, $page);
		
		return $result;
	}
		
	
	/**
	 *
	 */
	
	public function api_forge()
	{
		$this->forge();
	}
	
	
	/**
	 *
	 */
	
	public function api_load()
	{
		// Get API arguments
		
		$objectId = fv('objectId', true);
		$objectReference = fv('objectReference', false);
		$objectProperties = fv('objectProperties', false);
		
		
		// Call the method
		
		$result = $this->loadSecure($objectId, $objectReference, $objectProperties);
		
		
		return $result;
	}


	/**
	 *
	 */
	
	public function api_lock()
	{
		// Get API arguments
		
		$objectId = fv('objectId', true);
		$objectReference = fv('objectReference', false);
		
		
		// Call the method
		
		$this->loadSecure($objectId, $objectReference);

		$this->lock();

		$result = array
		(
			'objectId' => $this->getId(),
			'objectReference' => $this->getReference()
		);		
		
		return $result;
	}


	/**
	 * 
	 */

	public function api_preview()
	{
		// Get API arguments

		$objectId = fv('objectId', true);

		$this->load($objectId);

		$result = $this->preview();

		return $result;
	}
	
	
	/**
	 *
	 */
	
	public function api_restore()
	{
		// Get API arguments
		
		$objectId = fv('objectId', true);
		$objectReference = fv('objectReference', false);
		
		
		// Load the object

		$this->loadSecure($objectId, $objectReference);


		// Call the method
		
		$this->restore();
		
		
		// Prepare result
		
		$result = array
		(
			'urlOfRedirection' => $this->getUrlOfPage(ACTION_LIST)
		);
		
		
		return $result;
	}
	
	
	/**
	 *
	 */
	
	public function api_save()
	{		
		// Get API arguments
		
		$objectProperties = fv('objectProperties', true);
		$languageIsoCode = fv('languageIsoCode', false);
		$unlockObject = fv('unlockObject', false);


		// Load the object if needed
		
		if
		(
			(isset($objectProperties[$this->idName]) == true) &&
			(empty($objectProperties[$this->idName]) == false) &&
			(isset($objectProperties[$this->referenceName]) == true) &&
			(empty($objectProperties[$this->referenceName]) == false)
		)
		{
			$this->loadSecure($objectProperties[$this->idName], $objectProperties[$this->referenceName], null, array('acceptTemporaryObjects' => true));
			
			unset($objectProperties[$this->idName]);
			unset($objectProperties[$this->referenceName]);
		}

		
		// Call the method
		
		$ids = $this->save($objectProperties, $languageIsoCode);

		if 
		(
			(is_null($unlockObject) == false) && 
			($unlockObject == true)
		)
		{
			$this->unlock();
		}

		// Prepare result
		
		$result = array
		(
			'objectId' => $this->getId(),
			'objectReference' => $this->getReference(),
			'urlOfRedirection' => $this->getUrlOfPage(ACTION_VIEW)
		);

		if 
		(
			(is_array($ids) == true) &&
			(isset($ids['additionalIds']) == true)
		)
		{
			$result['additionalIds'] = $ids['additionalIds'];
		}
		
		
		return $result;
	}
	
	
	/**
	 *
	 */
	
	public function api_uploadFile()
	{
		// Get API arguments
		
		$index = fv('index', false);
		$objectId = fv('objectId', false);
		$objectReference = fv('objectReference', false);
		$propertyName = fv('propertyName', true);
		

		// Load the object or save it
		
		if
		(
			(empty($objectId) == false) && 
			(empty($objectReference) == false)
		)
		{
			$this->loadSecure($objectId, $objectReference, null, array('acceptTemporaryObjects' => true));
		}
		else
		{
			$this->setIsTemporary(true);
			$this->save();
		}
		
		
		// Call the method
		
		$result = $this->uploadFile($propertyName, $index);


		return $result;
	}


	/**
	 *
	 */

	public function api_unlock()
	{
		$objectId = fv('objectId', true);
		$objectReference = fv('objectReference', false);

		$this->loadSecure($objectId, $objectReference);

		$this->unlock();

		$result = array
		(
			'objectId' => $this->getId(),
			'objectReference' => $this->getReference(),
			'urlOfRedirection' => $this->getUrlOfPage(ACTION_VIEW)
		);

		return $result;
	}
	

	/**
	 * Lists all values contains string in param
	 * 
	 * @param 	string 	$value 			The value
	 * @param 	array 	$conditions		Associative array with different properties you want to use in find values
	 * 
	 * @return 	array 					Possible values
	 */

	public function autocomplete($value, $conditions = null)
	{
		// Remove whitespaces

		$value = trim($value);

		$internalConditions = array();


		// Is there any conditions ?

		if 
		(
			is_array($conditions) === true && 
			count($conditions) > 0
		)
		{	
			foreach ($conditions as $propertyName => $conditionValue)
			{
				if (is_array($conditionValue) === true)
				{
					if (count($conditionValue) != 2)
					{
						e
						(
							EXCEPTION_ARRAY_UNEXPECTED_NUMBER_OF_ELEMENTS, 
							array
							(
								'conditionValue' => $conditionValue
							)
						);
					}

					$internalConditions[$propertyName] = array($conditionValue[0], $conditionValue[1]);
				}
				else
				{
					$internalConditions[$propertyName] = array(EQUAL, $conditionValue);					
				}
			}
		}


		// Prepare parent property name element

		$parentPropertyName = 'parent' . ucfirst($this->objectName) .'Id';

		
		// Pattern to check if $value contains 'Parent element > Child Element'

		$pattern = '/(\w+)\s*>\s*(\w*)$/'; 

		// Get parent-element > first child element

		if (preg_match($pattern, $value, $matches) === 1)
		{
			// Extract the first element

			$parentElementValue = trim($matches[1]);
			
			// Get the appropriate value

			$value = trim($matches[2]);


			// Get the appropriate objects with this defaultName

			$rawParentObjects = $this->find
			(
				array
				(
					'conditions' => array
					(
						$this->defaultPropertyName => array(EQUAL, $parentElementValue)
					),
					'internalConditions' => $internalConditions
				)
			);

			$parentObjectsIds = array();

			
			// Get object ids

			if (count($rawParentObjects) > 0)
			{
				foreach($rawParentObjects as $rawParentObject)
				{
					$this->loadFromRawObject($rawParentObject);

					$parentObjectsIds[] = $this->getId();
				}
			}				

			$internalConditions[$parentPropertyName] = array(IN, $parentObjectsIds);		
		}

		
		// Build query conditions

		$listConditions = array();

		// What properties are set for autocompletion ?

		$autocompleteProperties = $this->autocompleteProperties;

		// Find the value into Object's autocomplete properties 

		foreach($autocompleteProperties as $propertyName)
		{
			$listConditions[$propertyName] = array(LIKE, '%' . $value . '%');
		}


		// Have we a parent property name ?

		if (empty($this->parentProperties) === false)
		{
			foreach($this->parentProperties as $parentProperty)
			{
				// ParentProperty is added only if it has not been into the array of conditions
				
				if (in_array($parentProperty, array_keys($conditions)) === false)
				{
					$internalConditions[$parentProperty] = array(EQUAL, fv($parentProperty));
				}				
			}
		}

		
		// 

		$hasRecursiveParent = false;

		// Get object properties names

		$objectPropertiesNames = $this->propertiesNames;

		if (in_array($parentPropertyName, $objectPropertiesNames) === true)
		{
			$property = $this->getProperty($parentPropertyName);

			// This object might have a parent ?

			if ($property['isAssociation'] == true)
			{
				$hasRecursiveParent = true;
			}
		}

			
		// Build an array of conditions

		$findOptions = array();

		// Conditions 

		$findOptions['conditions'] = $listConditions;

		// InternalConditions

		$findOptions['internalConditions'] = $internalConditions;

		// Limit

		$findOptions['limit'] = array(0, pref('[Loop]/ui/nbItemsPerPage'));

		// Logical Operator

		$findOptions['logicalOperator'] = LOOP_OR;

		// Ordered by parent object Id

		if ($hasRecursiveParent === true)
		{
			$findOptions['orderBy'] = array($propertyName);
		}

		// Order direction

		$findOptions['orderDirection'] = 'ascendent';

		
		// Find the corresponding elements

		$rawObjects = $this->find($findOptions);

		
		// An array of results
		
		$results = array();

		foreach ($rawObjects as $rawObject)
		{
			// Load the object

			$this->loadFromRawObject($rawObject);

			$names = array();

			$currentObject = $this;

			// Get default property name method

			$defaultPropertyNameMethod = 'getDefault' . ucfirst($this->defaultPropertyName);

			$names[] = $this->$defaultPropertyNameMethod();

			
			// Have we a recursive object ?

			if ($hasRecursiveParent === true)
			{
				// Get parent value

				$methodName = 'get' . ucfirst($propertyName);


				// Get each object parent names

				while 
				(
					is_null($currentObject->$methodName()) === false && 
					$currentObject->isLoaded() == true
				)
				{
					// Get parents objects

					$parentMethodName = 'getParent' . ucfirst($currentObject->objectName);

					$currentObject = $currentObject->$parentMethodName();

					// This object is not loaded, exit

					if ($currentObject->isLoaded() == false)
					{
						continue;
					}

					// Add object names

					$names[] = $currentObject->$defaultPropertyNameMethod();
				}
			}

			
			// The first element in array have to be the parents					

			$names = array_reverse($names);
			
			// Built object name as ParentName > FirstChildName > SecondChildName

			$stringNames = implode(' > ', $names);

			// Results

			$results[] = array
			(
				'classOfIcon' => $this->getClassOfDefaultIcon(),
				'description' => $this->getDefaultDescription(), 
				'id' => $this->getId(),
				'name' => $stringNames,
				'reference' => $this->getReference()
			);
		}
		

		return $results;
	}


	/**
	 * Clean table if exists
	 */
	
	public function clean()
	{		
		app()->getObjectFactory()->clean($this);
	}
	

	/**
	 * Converts an object to a raw object
	 *
	 * @return	array	An associative array, i.e. the object in raw form
	 */
			
	public function convertToRawObject()
	{
		$rawObject = app()->getObjectFactory()->convertToRawObject($this);

		return $rawObject;
	}

	
	/**
	 * Counts the total number of objects
	 *
	 * @param	array	$options	An array containing options
	 *
	 * @return	int					The number of objects found
	 */
	
	public function count($options = null)
	{		
		// Get the number of objects
		
		$nbObjects = app()->getObjectFactory()->count($this, $options);
		
		
		return $nbObjects;
	}
		

	/**
	 * Creates a feature
	 *
	 * @param	string	$extensionId				The name of extension
	 * @param	string	$featureName				The name of feature needed
	 */

	public function createFeature($extensionId, $featureName)
	{
		app()->getObjectFactory()->createFeature($this, $extensionId, $featureName);
	}	


	/**
	 * Classes extending CObject can define their own features by overwriting this function
	 */
	
	protected function createFeatures()
	{
	}
	

	/**
	 * Creates a filter
	 *
	 * @param	string	$filterName					The name of the filter
	 * @param	string	$filterProperties			The properties of the filter
	 * @param	string	$filterRelationalOperator	The relational operator associated with the filter
	 * @param	boolean	$isKey						True to enable the filter from start, false otherwise
	 * @param	boolean	$isHidden					True to hide from end-user, false otherwise
	 */
	
	public function createFilter($filterProperties, $filterRelationalOperator, $isKey = false, $isHidden = false)
	{
		app()->getObjectFactory()->createFilter($this, $filterProperties, $filterRelationalOperator, $isKey, $isHidden);
	}	


	/**
	 * Classes extending CObject can define their own filters by overwriting this function
	 */
	
	protected function createFilters()
	{
	}
	
	
	/**
	 * Filled an array which will serve to create indexes at the time of forge
	 * 
	 * @param array $indexedProperties Contains columns concerned by the creation of the index
	 */
	
	protected function createIndex($indexedProperties = array())
	{
		app()->getObjectFactory()->createIndex($this, $indexedProperties);
	}
	

	/**
	 * Creates a property. It can either be localized or not, or a smart property
	 *
	 * @param	string	$propertyName		The name of the property
	 * @param	string	$propertyOptions	The options of the property (e.g. is it a primary key, pattern to validate the value)
	 */
	
	public function createProperty($propertyName, $propertyOptions)
	{
		// It the property name too long?

		if (strlen($propertyName) > pref('[Loop]/object/propertyName/maxLength'))
		{
			e
			(
				EXCEPTION_OBJECT_PROPERTY_NAME_TOO_LONG,
				array
				(
					'length' => strlen($propertyName),
					'maxLength' => pref('[Loop]/object/propertyName/maxLength'),
					'propertyName' => $propertyName
				)
			);
		}


		// Create an array that contains all possible options with default value
		
		$defaultPropertyOptions = array
		(
			'className' => null,
			'defaultValue' => null,
			'extensionId' => null,
			'fallbackProperty' => null,
			'fallbackPropertyName' => null,
			'fieldType' => null,
			'hasChanged' => null,
			'isAggregation' => null,
			'isAncester' => null,
			'isAssociation' => null,
			'isAutocomplete' => null,
			'isAutomatic' => null,
			'isCore' => null,
			'isCustom' => null,
			'isIncremental' => null,
			'isComposition' => null,
			'isConfidential' => null,
			'isDescription' => null,
			'isDirect' => null,
			'isExclusive' => null,
			'isFile' => null,
			'isFormatted' => null,
			'isIcon' => null,
			'isImage' => null,
			'isImageCropped' => null,
			'isImageResized' => null,
			'isLocalized' => null,
			'isName' => null,
			'isParent' => null,
			'isPrimaryKey' => null,
			'isRequired' => null,
			'isSmart' => null,
			'isTemporary' => null,
			'isUnique' => null,
			'isUnregistered' => null,
			'maxHeight' => null,
			'maxWidth' => null,
			'object' => null,
			'pattern' => null,
			'pathToExtension' => null,
			'propertyName' => null,
			'relationName' => null,
			'sqlType' => null,
			'type' => null,
			'value' => null,
			'values' => null
		);
		
		
		// Merge the full one with the one for the property and secure the name
	
		$propertyOptions = array_merge($defaultPropertyOptions, $propertyOptions);

		
		// Define which should be the target array storage
		
		$targetProperties = null;
		
		if ($propertyOptions['isFile'] == true)
		{
			// Target is the list of file properties
			
			$targetProperties = &$this->fileProperties;


			// Define the field type

			$propertyOptions['fieldType'] = 'File';
		}
		else if
		(
			($propertyOptions['isAggregation'] == true) || 
			($propertyOptions['isSmart'] == true)
		)
		{
			// It has to be a smart property
			
			$propertyOptions['isSmart'] = true;
			
			
			// Target is the list of smart properties
			
			$targetProperties = &$this->smartProperties;
		}
		else if ($propertyOptions['isComposition'] == true)
		{
			$targetProperties = &$this->smartProperties;	
		}
		else if ($propertyOptions['isLocalized'] == true)
		{
			// We have a localized property, therefore the whole object is localized
			
			$this->isLocalized = true;
			
			
			// Target is the list of localized properties
			
			$targetProperties = &$this->localizedProperties;

			$languages = pref('[Loop]/localization/languages');
			$options['hasChanged'] = array();
			
			foreach ($languages as $languageIsoCode)
			{
				$options['hasChanged'][$languageIsoCode] = null;
			}
			
			
			// This property will be automatically loaded when doing a load/find
			// Except if it's languageIsoCode
			
			if ($propertyName != 'languageIsoCode')
			{
				array_push($this->propertiesNames, $propertyName);
			}
		}
		else
		{
			// If the property is an association, make sure it has the right type
			
			if ($propertyOptions['isAssociation'] == true)
			{
				// Define mandatory options
				
				$propertyOptions['fieldType'] = 'Select';
				$propertyOptions['isDirect'] = true;
				$propertyOptions['type'] = TYPE_ID;
				
				
				// Should we inherit from properties?
				
				if ($propertyOptions['isAncester'] == true)
				{
					$this->namesOfAncesters[] = $propertyName;
				}
			}
			
			
			// Target is the list of normal properties of the array storage
			
			$targetProperties = &$this->staticProperties;
			
			// This property will be automatically loaded when doing a load/find
		
			array_push($this->propertiesNames, $propertyName);
		}
		

		// Extract the SQL type and pattern

		if (is_null($propertyOptions['type']) === false)
		{
			app()->getObjectFactory()->processPropertyType($propertyOptions);
		}


		// Is the SQL type equal to ENUM?

		if ($propertyOptions['sqlType'] == 'ENUM')
		{
			// Yep, then extract available values from the pattern

			$rawEnumValues = substr($propertyOptions['pattern'], 3, strlen($propertyOptions['pattern']) - 6);
			$enumValues = explode('|', $rawEnumValues);


			// Surround each value by quotes

			$finalEnumValues = array();

			foreach ($enumValues as $enumValue)
			{
				$finalEnumValues[] = '\'' . $enumValue . '\'';

				$propertyOptions['values'][$enumValue] = $enumValue;
			}


			// Write the final ENUM

			$propertyOptions['sqlType'] = 'ENUM(' . implode(',', $finalEnumValues) . ')';
		}


		// Have we received an extension ID? If so convert it to a path

		if (empty($propertyOptions['extensionId']) === false)
		{
			$propertyOptions['pathToExtension'] = app()->getPathHelper()->convertExtensionIdToPath($propertyOptions['extensionId']);
		}


		// Add the property to the target list of properties

		if($propertyOptions['isUnregistered'] == false)
		{
			$targetProperties[$propertyName] = $propertyOptions;
		}
		
		
		// Is the property the default name / description / icon?
		
		if ($propertyOptions['isDescription'] == true)
		{
			$this->defaultPropertyDescription = $propertyName;
		}
		else if ($propertyOptions['isIcon'] == true)
		{
			$this->defaultPropertyIcon = $propertyName;
		}
		else if ($propertyOptions['isName'] == true)
		{
			$this->defaultPropertyName = $propertyName;
		}
		else if ($propertyOptions['isAutocomplete'] == true)
		{
			$this->defaultPropertyDescription = $propertyName;
			$this->defaultPropertyIcon = $propertyName;
			$this->defaultPropertyName = $propertyName;
		}


		// Is the property a parent?

		if ($propertyOptions['isParent'] == true)
		{
			$this->parentProperties[] = $propertyName;
		}

		
		// Is the property to be used by autocomplete()?
		// @todo Index should be on all autocomplete properties

		if ($propertyOptions['isAssociation'] == true)
		{
			//$this->createIndex(array($propertyName));
		}
		else if ($propertyOptions['isAutocomplete'] == true)
		{
			$this->autocompleteProperties[] = $propertyName;
			$this->createIndex(array($propertyName));
		}
	}


	/**
	 * Classes extending CObject can define their own properties by overriding this function
	 */
	
	protected function createProperties()
	{
	}
	
	
	/**
	 * Deletes an existing object
	 */
	
	public function delete()
	{
		// Start the transaction

		$this->databaseDriver->startTransaction();


		// Is the object in the database?
		
		if ($this->isLoaded() == true)
		{
			// Delete all exclusive associations plus compositions
			
			$this->deleteAssociations();
			$this->deleteCompositions();

			
			// To delete an object, simply set a time of deletion

			$this->setSourceOfDeletion($this->extensionId);
			$this->setTimeOfDeletion(time());
			$this->setUserDeleterId(user()->getId());

			$this->save();


			// Delete all files
			
			$pathToFiles = path
			(
				array
				(
					app()->getPathHelper()->convertExtensionIdToPath($this->extensionId),
					'Data',
					'Object',
					$this->classNameWithoutNamespace
				),
				false
			);

			$pattern = $this->getId() . '_' . $this->getReference() . '*';

			$paths = app()->getFileHelper()->listFiles($pathToFiles, $pattern);

			foreach ($paths as $path)
			{
				unlink($path);
			}
		}
				
		
		// Now reset the object
		
		$this->reset();


		// Close the transaction

		$this->databaseDriver->closeTransaction();
	}


	/**
	 *
	 */

	public function deleteAssociations()
	{
		foreach ($this->staticProperties as $propertyName => &$property)
		{
			if
			(
				($property['isAssociation'] === true) &&
				($property['isExclusive'] === true)
			)
			{
				// Delete the association
				
				$object = app()->getObjectFactory()->convertPropertyToObject($this, $propertyName);
				
				try
				{
					$object->delete();
				}
				catch (Exception $e)
				{
				}
			}
		}
	}


	/**
	 *
	 */

	public function deleteCompositions($propertyName = null)
	{
		// Which properties do we want to delete?

		if (empty($propertyName) === true)
		{
			$properties = $this->smartProperties;
		}
		else
		{
			$properties = array
			(
				$propertyName => $this->getProperty($propertyName)
			);
		}


		// Do it!

		foreach ($properties as $propertyName => &$property)
		{
			if
			(
				($property['isComposition'] === true) &&
				($property['isSmart'] == false)
			)
			{
				// Delete all relations (not target objects)
				
				$result = app()->getObjectFactory()->convertPropertyToRawRelations($this, $propertyName, null, $property['propertyName']);
				
				foreach ($result['rawRelations'] as $rawRelation)
				{
					$result['relation']->loadFromRawObject($rawRelation);
					
					try
					{
						$result['relation']->delete();
					}
					catch (Exception $e)
					{
					}
				}
			}
		}
	}


	/**
	 * Deletes a file attached to a property
	 *
	 * @param	string	$propertyName	The name of the property
	 * @param	int		$index			The index of the file
	 */
	
	public function deleteFile($propertyName, $index)
	{
		// Get the property

		$property = $this->getProperty($propertyName);

		
		// Does this property belong to an ancester?
		// If so ask the ancester for the URL

		if (isset($property['ancester']) == true)
		{
			return $property['ancester']->deleteFile($propertyName, $index);
		}


		// Find all file where contains an exact property name and index. (file with different variant, icon...)

		$pathToFiles = path
		(
			array
			(
				app()->getPathHelper()->convertExtensionIdToPath($this->extensionId),
				'Data',
				'Object',
				$this->classNameWithoutNamespace
			)
		);

		$globalPath = $pathToFiles . $this->getId() . '_' . $this->getReference() . '_'. $propertyName;
		$fileIndexes = glob($globalPath . '*');

		$pattern = $this->getId() . '_' . $this->getReference() . '_'. $propertyName . '*';

		$fileIndexes = app()->getFileHelper()->listFiles($globalPath, $pattern);


		// Search all indexes for a property name to prepare reaffectation of index after delete one.

		$indexes = array();
		$indexAffectations = array();

		foreach ($fileIndexes as $fileIndex)
		{
			$pattern = '/i-([0-9]+)/';

			preg_match($pattern, $fileIndex, $matches);

			if (isset($matches[1]) == true)
			{
				if (array_key_exists($matches[1], $indexAffectations) == false)
				{
					$indexAffectations[$matches[1]] = $matches[1];
				}
			}
		}


		// Reaffect all indexes

		$indexAffectations[$index] = 'null';

		$nbIndexes = count($indexAffectations);

		for ($i = $index + 1; $i <= $nbIndexes; $i++) { 
			$indexAffectations[$i] = $i - 1;
		}


		// Delete the main file

		$pattern = $this->getId() . '_' . $this->getReference() . '_'. $propertyName . '_*i-' . $index . '*.*';
		$files = app()->getFileHelper()->listFiles($pathToFiles, $pattern);
				
		foreach ($files as $file)
		{
			unlink($file);
		}
		

		// Rename remaining files

		foreach ($indexAffectations as $key => $value)
		{
			if
			(
				($key != $value) && 
				($value != 'null')
			)
			{
				$files = app()->getFileHelper()->listFiles($pathToFiles, $pattern);

				foreach ($files as $file)
				{
					$pattern = '/i-([0-9]+)/';
					$newName = preg_replace($pattern, 'i-'.$value, $file);

					rename($file, $newName);
				}
			}
		}
		

		return $indexAffectations;
	}
	
	
	/**
	 * Disables an object
	 */
	
	public function disable()
	{
		// Set the time of disabling to now

		$this->setTimeOfDisabling(time());
		$this->setUserDisablerId(user()->getId());
		

		// Save the object if it already exists
		
		if ($this->isLoaded() == true)
		{
			$this->save();
		}
	}


	/**
	 * Enables an object
	 */
	
	public function enable()
	{
		// Set the time of disabling to never

		$this->setTimeOfDisabling(null);
		$this->setUserDisablerId(null);
		

		// Save the object if it already exists

		if ($this->isLoaded() == true)
		{
			$this->save();
		}
	}
	
	
	/**
	 * Filters objects according to a set of conditions
	 *
	 * @param	array		$filters			The associative array which contains all data for filters [key](mixed type of value) => 'value' (mixed type of value)
	 * @param	string	    $logicalOperator	The logical operator associated to all filters (AND | OR)
	 * @param	int			$page				The current page number
	 *
	 * @return	array $result An array of raw objects + the total number of objects
	 */
	
	public function filter($filters, $logicalOperator, $page = null, $crossLanguages = false)
	{
		// Make sure $filters is an array

		if (is_array($filters) == false)
		{
			$filters = array();
		}
			
				
		// Ensure the page number is valid
		
		if ($page < 1)
		{
			$page = 1;
		}
		
		
		// Build the options
		
		$options = array
		(
			'conditions' => array(),
			'logicalOperator' => $logicalOperator
		);
				
		
		// Extract filter names and there values, and filter operator and there values
		
		$filterNames = array();
		$filterOperators = array();
		

		// Extract filter names 
		
		if (isset($filters['filterNames']) && is_array($filters['filterNames']))
		{
			$filterNames = $filters['filterNames'];
			unset($filters['filterNames']);
		}
		
		
		// Extract filter operators
		
		if (isset($filters['filterOperators']) == true && is_array($filters['filterOperators']) == true)
		{
			$filterOperators = $filters['filterOperators'];
			unset($filters['filterOperators']);
		}
		
		
		// Combine initial filters values, and the array which have been extractes 
		
		if ( is_array($filterNames) == true && is_array($filters) == true )
		{
			$filters = array_merge($filterNames, $filters);
		}
		
		
		// Localized filter property
				
		$localizedFilters = array();
		
		
		// Build options
		
		$optionsLocalizedFilters = array
		(
			'conditions' => array(),
			'logicalOperator' => $logicalOperator
		);
		
					
		// Add a new condition for each filter found

		$hasIntentionalFilters = false;
		$filtersFromAncesters = array();

		foreach ($filters as $filterName => $filterValue)
		{
			// Get the filter
			
			$filter = $this->getFilter($filterName);

			if (empty($filter['ancester']) == false)
			{
				// Yep, keep it for later

				if (isset($filtersFromAncesters[$filter['ancester']]) == false)
				{
					$filtersFromAncesters[$filter['ancester']] = array();
				}

				$ancesterFilterNames = array($filterName => $filterValue);
				$ancesterFilterOperators = array($filterName. '_operator' => $filterOperators[$filterName. '_operator']);

				if(empty($filtersFromAncesters[$filter['ancester']]['filterNames']) == false)
				{
					$filtersFromAncesters[$filter['ancester']]['filterNames'] = array_merge($filtersFromAncesters[$filter['ancester']]['filterNames'], $ancesterFilterNames);
				}
				else
				{
					$filtersFromAncesters[$filter['ancester']]['filterNames'] = $ancesterFilterNames;
				}

				if(empty($filtersFromAncesters[$filter['ancester']]['filterOperators']) == false)
				{
					$filtersFromAncesters[$filter['ancester']]['filterOperators'] = array_merge($filtersFromAncesters[$filter['ancester']]['filterOperators'], $ancesterFilterOperators);
				}
				else
				{
					$filtersFromAncesters[$filter['ancester']]['filterOperators'] = $ancesterFilterOperators;
				}

				continue;
			}


			// Check if the relationalOperator is an array
			// In that case check if the operator we get for this filterName exists in the array of relationalOperator 
			// Then if this one exists, then set the corresponding operator that will be used in options conditions 
			
			if (is_array($filter['relationalOperator']) == true)
			{
				$relationalOperatorKeys = array_keys($filter['relationalOperator']);
				
				if
				(
					(isset($filterOperators[$filterName . '_operator']) == false) ||
					(in_array($filterOperators[$filterName . '_operator'], $relationalOperatorKeys) == false)
				)
				{
					return;
				}
				else
				{
					$filter['relationalOperator'] = $filterOperators[$filterName . '_operator'];
				}				
			}
			
			// LIKE requires to surround the value with %
			
			if (
					$filter['relationalOperator'] == 'LIKE' ||
					$filter['relationalOperator'] == 'NOT LIKE'
					
				)
			{ 
				$filterValue = '%' . $filterValue . '%';
			}
			
			
			// Get the property's settings 
			
			$property = $this->getProperty($filterName);
			
			
			// Extract each localized property
			
			if ($property['isLocalized'] == true)
			{
				$localizedFilters[$filterName] = array($filter['relationalOperator'] , $filterValue);
			}

			
			// Add the filter to the right kind of condition (internal if hidden, otherwise a normal condition)
			// Internal conditions are associated with a AND
			// Whereas normal conditions are going to use AND/OR (stored in $logicalOperator)
			
			if ($filter['isHidden'] == true)
			{
				$options['internalConditions'][$filterName] = array($filter['relationalOperator'], $filterValue);
			}
			else
			{
				$hasIntentionalFilters = true;
				$options['conditions'][$filterName] = array($filter['relationalOperator'], $filterValue);
			}
		}


		// Filter localized property which where sets
		
		if ( count($localizedFilters) > 0 )
		{
			// if have a localized filter, filter the current language
			
			if($crossLanguages == false)
			{
				$options['internalConditions']['languageIsoCode'] =  array(EQUAL, pref('[Loop]/localization/languageIsoCode'));
			}
		}

		// Perform filter on ancester properties

		$hasConditionsFromAncesters = false;
		$hasIntentionalFiltersFromAncesters = false;

		if(empty($filtersFromAncesters) == false)
		{
			$idsFromAncesters = array();

			foreach ($filtersFromAncesters as $ancesterName => $filtersFromAncester)
			{
				foreach ($filtersFromAncester['filterNames'] as $filterName => $filterValue)
				{
					$filter = $this->getFilter($filterName);

					if ($filter['isHidden'] == false)
					{
						$hasIntentionalFiltersFromAncesters = true;
					}
				}

				$ancester = app()->getObjectFactory()->convertPropertyToObject($this, $ancesterName);

				$resultFromAncesters = $ancester->filter($filtersFromAncester, $logicalOperator, $page, $crossLanguages);

				$idsFromAncesters[$ancesterName] = array_merge($idsFromAncesters, array_keys($resultFromAncesters['rawObjects']));
			}

			if (empty($idsFromAncesters) == false)
			{
				foreach($idsFromAncesters AS $ancesterName => $idsFromAncester)
				{
					if
					(
						(empty($idsFromAncester) == false) &&
						($hasIntentionalFiltersFromAncesters == true)
					)
					{
						$hasConditionsFromAncesters = true;
					}

					$options['conditions'][$ancesterName] = array(IN, $idsFromAncester);
				}
			}
		}


		//

		if
		(
			($hasIntentionalFilters == false) &&
			($hasIntentionalFiltersFromAncesters == true) &&
			($hasConditionsFromAncesters == false)
		)
		{
			return array
			(
				'nbObjects' => 0,
				'rawObjects' => array()
			);
		}


		// Get the number of objects
		
		$nbObjects = $this->count($options);

		
		// Get raw objects
		
		$options['limit']= array
		(
			($page - 1) * pref('[Loop]/ui/nbItemsPerPage'),
			pref('[Loop]/ui/nbItemsPerPage')
		);
						
		$rawObjects = $this->find($options);


		// Prepare results
		
		$result = array
		(
			'nbObjects' => $nbObjects,
			'rawObjects' => $rawObjects
		);


		return $result;
		
	}	
	
	/**
	 * Finds objects as defined in $options (if empty, looks for all objects) and returns raw objects
	 *
	 * @param	array $options An array containing options
	 *
	 * @return	array The objects found (raw)
	 */
	
	public function find($options = null)
	{
		//

		plog(null, $options);


		// Do we have the right to perform this action on the class?
		
		app()->getSecurityManager()->grantPermissionOnClass($this->className, ACTION_FIND);


		// Process options
		
		$properties = array();
		$smartProperties = array();
		$queryFragments = array();
	
		app()->getObjectFactory()->processFindOptions($this, $options, $properties, $smartProperties, $queryFragments);

		
		// Write the SELECT part
		
		$query = 'SELECT ';
		$separator = null;

		foreach ($properties as $propertyName)
		{
			$query .= $separator . '`%1`.`%2`.`' . $propertyName . '`';
			$separator = ', ';
		}

		if (empty($smartProperties) == false)
		{
			$query .= $separator . implode(', ', $smartProperties);
		}		


		// Write the FROM part

		$query .= ' FROM `%1`.`%2` ';


		// Write the rest (WHERE, ORDER BY, GROUP BY, etc...)

		$query .= implode(' ', $queryFragments);

		
		// Run the query

		$queryVariables = array($this->databaseName, $this->viewName);
		$queryResults = $this->databaseDriver->executeQuery($query, $queryVariables);

		
		// Put all raw objects into an array
		
		$rawObjects = app()->getObjectFactory()->processFindResult($this, $queryResults);


		return $rawObjects;
	}
	
	
	/**
	 * Installs an object, i.e. creates tables and views
	 */
	
	public function forge()
	{
		app()->getObjectFactory()->forge($this);
	}


	/**
	 *
	 */

	public function generateImageAndThumbnail($pathImageFile, $propertyName, $index, $class, $variant, $size, $fileExtension)
	{		
		// Does the property exist?

		$property = $this->getProperty($propertyName);


		// It is, we'll need the image helper to perform a few things on the image
		
		$imageHelper = app()->getImageHelper();
		
		
		// First, resize and crop the image if required

		if ($property['isImageResized'] == true)
		{
			$imageHelper->resize($pathImageFile, $pathImageFile, $property['maxWidth'], $property['maxHeight']);				
		}
		
		if ($property['isImageCropped'] == true)
		{
			$imageHelper->crop($pathImageFile, $pathImageFile, $property['maxWidth'], $property['maxHeight']);				
		}
		
		
		// Second, generate thumbnails for this image

		$imageHelper->generateThumbnails($pathImageFile, $this, $propertyName, $index, $class, $variant, $size, $fileExtension);	
	}


	/**
	 * Return ancesters for an object.
	 */

	public function getAncesters()
	{
		$ancesters = array();

		foreach ($this->namesOfAncesters as $ancesterName)
		{
			$ancester = app()->getObjectFactory()->convertPropertyToObject($this, $ancesterName);
			$ancesters[$ancesterName] = $ancester;
		}

		return $ancesters;
	}

	
	/**
	 * Gets the class of the default icon of the object
	 */
	
	public function getClassOfDefaultIcon()
	{
		// Simply return the object name for now
		// Objects extending CObject should overwrite this one
		
		return $this->objectName;
	}
	
	
	/**
	 * Gets the default description of the object
	 *
	 * @return	string The default description
	 */
	
	public function getDefaultDescription()
	{
		try
		{
			return $this->getPropertyValue($this->defaultPropertyDescription);
		}
		catch (\Exception $e)
		{
		}
	}
	
	
	/**
	 * Gets the default name of the object
	 */
	
	public function getDefaultName()
	{
		try
		{
			return $this->getPropertyValue($this->defaultPropertyName);
		}
		catch (\Exception $e)
		{
			return $this->className;
		}
	}
	
	
	/**
	 * Gets the first extension available for a file property
	 *
	 * @param	string	$propertyName	The name of the property
	 * @param	string	$index			The index of the file (e.g. 1, 2, 3)
	 *
	 * @return	string					The extension of the file
	 */
	
	public function getExtensionOfFile($propertyName, $index = null)
	{
		// Get the property
		
		$property = $this->getProperty($propertyName);
		
		
		// Find which extension is available
		
		if
		(
			(isset($property['extensions']) == true) &&
			(is_array($property['extensions']) == true)
		)
		{
			$pathToFiles = path
			(
				array
				(
					app()->getPathHelper()->convertExtensionIdToPath($this->extensionId),
					'Data',
					'Object',
					$this->classNameWithoutNamespace
				)
			);

			$pattern = $this->getId() . '_' . $this->getReference() . '_' . $propertyName . '_*i-' . $index . '*.*';

			$files = app()->getFileHelper()->listFiles($pathToFiles, $pattern);


			if (count($files) > 0)
			{
				$file = array_shift($files);
				$extension = substr($file, strrpos($file, '.') + 1);

				return $extension;
			}
		}
	}


	/**
	 * Get a variable specific for a feature
	 *
	 * @param 	string 	$extensionId 	extension of feature
	 * @param 	string 	$featureName 	name of feature
	 * @param 	string 	$variableName 	name of variable needed
	 *
	 * @return 	object 
	 */

	public function getFeatureVariable($extensionId, $featureName, $variableName)
	{
		$variable = null;


		if (is_array($this->featureVariables) == false)
		{
			$this->featureVariables = array();
			$this->featureVariables[$extensionId][$featureName][$variableName] = '';
		}
		
		

		if (isset($this->featureVariables[$extensionId][$featureName][$variableName]) == false)
		{
			e
			(
				EXCEPTION_FEATURE_VARIABLE_NOT_FOUND,
				array
				(
					'extensionId' => $extensionId,
					'featureName' => $featureName,
					'variableName' => $variableName
				)
			);
		}

		$variable = $this->featureVariables[$extensionId][$featureName][$variableName];

		return $variable;
	}


	/**
	 * Gets the filename of a property
	 *
	 * @param	string	$propertyName	The name of the property
	 * @param	string	$index			The index of the file (e.g. 1, 2, 3). If null Loop will ignore it
	 * @param	string	$class			The class of the image (TBD)
	 * @param	string	$variant		The variant of the image (e.g. 'thumbnail')
	 * @param	string	$size			The longest side of the image (e.g. 16, 64, 512)
	 * @param	string	$extension		The extension to be used (e.g. png)
	 *
	 * @return	string					The path to the file
	 */
	
	public function getFilename($propertyName, $index = null, $class = null, $variant = null, $size = null, $extension = null)
	{
		// Index must at least be 1
		
		if ($index < 1)
		{
			$index = 1;
		}


		// Build fragments of filename

		$fragments = array();

		$fragments[] = $this->getId();
		$fragments[] = $this->getReference();
		$fragments[] = $propertyName;
		$fragments[] = 'i-' . $index;

		if (empty($variant) == false)
		{
			$fragments[] = 'v-' . $variant;
		}
		
		if (empty($size) == false)
		{
			$fragments[] = 's-' . $size;
		}

		if (empty($class) == false)
		{
			$fragments[] = 'c-' . $class;
		}


		// Find the extension if needed
		
		if (empty($extension) === true)
		{
			$extension = $this->getExtensionOfFile($propertyName, $index);
		}
		
		
		// Build the final filename

		$filename = implode('_', $fragments) . '.' . $extension;


		return $filename;
	}
	
	
	/**
	 * Gets a filter
	 *
	 * @param	string $filterName The name of the filter
	 *
	 * @return	array The filter
	 */
	
	public function getFilter($filterName)
	{
		// Does the filter belong to the object?
				
		if (isset($this->filters[$filterName]) == true)
		{
			$filter = $this->filters[$filterName];
			$filter['ancester'] = null;

			return $filter;
		}


		// Does the filter belong to one of the ancesters?

		foreach ($this->namesOfAncesters as $ancesterName)
		{
			$ancester = app()->getObjectFactory()->convertPropertyToObject($this, $ancesterName);

			try
			{
				$filter = $ancester->getFilter($filterName);
				$filter['ancester'] = $ancesterName;

				return $filter;
			}
			catch (\Exception $e)
			{
				if ($e->getCode() != EXCEPTION_OBJECT_FILTER_NOT_FOUND)
				{
					throw $e;
				}
			}
		}


		// If we're here, it means there's no such filter

		e
		(
			EXCEPTION_OBJECT_FILTER_NOT_FOUND,
			array
			(
				'className' => $this->className,
				'filterName' => $filterName,
			)
		);
	}
	
	
	/**
	 * Gets the filters associated with the object
	 */
	
	public function getFilters()
	{
		$filters = $this->filters;

		foreach ($this->namesOfAncesters as $ancesterName)
		{
			$ancester = app()->getObjectFactory()->convertPropertyToObject($this, $ancesterName);
			$filters = array_merge($filters, $ancester->getFilters());
		}


		return $filters;
	}
		
	
	/**
	 * Gets the latest objects created
	 *
	 * @param	int	$nbObjects	The number of latest objects to return
	 * @param	array	$properties	Object properties to be loaded
	 *
	 * @return	array Raw objects found
	 */
	
	public function getLatest($nbObjects, $properties = null)
	{
		// Get the latest entries for this object
		
		$rawObjects = $this->find
		(
			array
			(
				'limit' => array(0, $nbObjects),
				'orderBy' => array('timeOfCreation'),
				'properties' => array_merge(array('timeOfCreation'), $properties)
			)
		);
		
		
		return $rawObjects;
	}
	
	
	/**
	 *
	 */
	
	public function getObjectUri()
	{
		// Set the URI fragment if none defined yet
		
		if (empty($this->objectUri) == true)
		{
			// Convert the object name xxxYYY into a xxx-yyy name
			
			$this->objectUri = preg_replace
			(
				'/(.)([A-Z])/',
				'\\1-\\2',
				$this->objectName
			);
			
			$this->objectUri = preg_replace
			(
				'/(.)_/',
				'\\1s-\\2',
				$this->objectUri
			);
			
			$this->objectUri = strtolower($this->objectUri);
			
			
			// Turn singular object name to plural
			
			$nbCharacters = strlen($this->objectUri);
			$lastCharacter = $this->objectUri[$nbCharacters - 1];
			$nextTolastCharacter = $this->objectUri[$nbCharacters - 2];
			
			if ($lastCharacter == 's')
			{
				$this->objectUri .= 'es';
			}
			else if ($lastCharacter == 'y')
			{
				if ($nextTolastCharacter == 'e')
				{
					$this->objectUri .= 's';
				}
				else
				{
					$this->objectUri = substr($this->objectUri, 0, $nbCharacters - 1);
					$this->objectUri .= 'ies';
				}
			}
			else
			{
				$this->objectUri .= 's';
			}
		}
		
		
		return $this->objectUri;
	}
	
	
	/**
	 * Retrieves the list of parents for the object
	 *
	 * @return	array An array of parent objects (loaded or not)
	 */
	
	public function getParents()
	{
		// If no parents yet, try to get them

		if
		(
			(empty($this->parents) == true) &&
			(empty($this->parentProperties) == false)
		)
		{
			// For each parent property

			foreach ($this->parentProperties as $propertyName)
			{
				try
				{
					// First, get the property

					$property = $this->getProperty($propertyName);
					

					// Is there a namespace ?

					$filename = $property['className'];

					if (file_exists($property['pathToExtension'].'Model/' . $filename . '.php') === false)
					{
						$namespace = str_replace('-', '\\', $property['extensionId']) . '\\';

						$filename = str_replace($namespace , '', $filename);
					}
					

					// Dependencies

					require_once($property['pathToExtension'] . 'Model/' . $filename . '.php');


					// Create the parent

					// Is there a namespace ?


					if (class_exists($property['className']) === false)
					{
						$parentClassName = str_replace('-', '\\', $property['extensionId']) . '\\' . $property['className'];
					}
					
					$parent = new $parentClassName();


					// Load the parent

					if ($this->isLoaded() == true)
					{
						// Object is loaded = piece of cake

						$methodName = 'get' . ucfirst($parent->objectName);

						$parent = $this->$methodName();
					}
					else
					{
						// Load the parent from arguments (most likely on the URL)
						
						$parentId = fv($parent->objectName . 'Id', true);
						$parentReference = fv($parent->objectName . 'Reference', false);

						$parent->loadSecure($parentId, $parentReference);
					}


					// Store the parent
					
					$this->parents[$propertyName] = &$parent;
				}
				catch (\Exception $e)
				{
					// Parent cannot be created, store nothing

					$this->parents[$propertyName] = null;
				}
			}
		}
		
		
		return $this->parents;
	}


	/**
	 * Return the parent of an object
	 *
	 * @param 	string 	$parentName 	The name of the parent class without 'C'
	 * @return 	object 	$parent 		The parent require
	 */

	public function getParent($parentName)
	{
		// Get parents of object

		$parents = $this->getParents();


		// Do we have this parent?

		if (isset($parents[$parentName]) == false)
		{
			e
			(
				EXCEPTION_OBJECT_PARENT_NOT_FOUND,
				array
				(
					'className' => $this->className,
					'parentName' => $parentName
				)
			);
		}


		return $parents[$parentName];
	}


	/**
	 * Return a parent property which does not have a fallback. If it use on the homepage use try catch.
	 *
	 * @param 	string 	$parentName 	The name of the parent class without 'C'
	 * @param 	string 	$propertyName 	The name of the property
	 * @return 	string 	$propertyValue 	The value of the property
	 */

	public function getParentPropertyValue($parentPropertyName, $propertyName)
	{

		$parent = $this->getParent($parentPropertyName);

		$getProperty = 'get' . ucfirst($propertyName);

		$propertyValue = $parent->$getProperty();

		return $propertyValue;
	}


	/**
	 * Determine if path extension for a property is the current path or an ancester's path.
	 * 
	 * @param string 	$propertyName 		Name of property
	 * @param string 	$pathsToExtension 	Actual path
	 * @param object 	$object 			If ancester contain the property is needed
	 */

	public function getPathExtension($propertyName, &$pathToExtension, &$object = null)
	{
		$ancesters = $this->getAncesters();

		foreach ($ancesters as $ancester)
		{
			try
			{
				$property = $ancester->getProperty($propertyName);

				if (is_array($property) == true)
				{
					$pathToExtension = app()->getPathHelper()->convertExtensionIdToPath($ancester->extensionId);

					if (is_null($object) == false)
					{
						$object = $ancester;
					}
				}
			}
			catch (\Exception $e)
			{
				continue;
			}
		}
	}
	
	
	/**
	 * Gets the path to the default icon of the object
	 */
	
	public function getPathToDefaultIcon()
	{
		try
		{
			return $this->getPathToFile($this->defaultPropertyIcon);
		}
		catch (\Exception $e)
		{
		}
	}
	
	
	/**
	 * Gets the path of a file property
	 *
	 * @param	string	$propertyName	The name of the property
	 * @param	string	$index			The index of the file (e.g. 1, 2, 3). If null Loop will ignore it
	 * @param	string	$class			The class of the image (TBD)
	 * @param	string	$variant		The variant of the image (e.g. 'thumbnail')
	 * @param	string	$size			The longest side of the image (e.g. 16, 64, 512)
	 * @param	string	$extension		The extension to be used (e.g. png)
	 *
	 * @return	string					The path to the file
	 */
	
	public function getPathToFile($propertyName, $index = null, $class = null, $variant = null, $size = null, $extension = null)
	{
		// Get the property

		$property = $this->getProperty($propertyName);

		
		// Does this property belong to an ancester?
		// If so ask the ancester for the URL

		if (isset($property['ancester']) == true)
		{
			return $property['ancester']->getPathToFile($propertyName, $index, $class, $variant, $size, $extension);
		}


		// Build the path

		$pathToFile = path
		(
			array
			(
				app()->getPathHelper()->convertExtensionIdToPath($this->extensionId),
				'Data',
				'Object',
				$this->classNameWithoutNamespace
			)
		);

		$pathToFile .= $this->getFilename($propertyName, $index, $class, $variant, $size, $extension);
		

		return $pathToFile;
	}
	
	
	/**
	 * Gets all properties of the object in one associative array
	 *
	 * @return	array An array with all properties, sorted by type of property
	 */
	
	public function getProperties()
	{
		// Merge all properties together
		
		$properties = array
		(
			'fileProperties' => $this->fileProperties,
			'localizedProperties' => $this->localizedProperties,
			'smartProperties' => $this->smartProperties,
			'staticProperties' => $this->staticProperties
		);
		
		
		return $properties;
	}
	
	
	/**
	 * Gets a property
	 *
	 * @param	string $propertyName The name of the property to get
	 *
	 * @return	array A reference to the property
	 */
	
	public function getProperty($propertyName)
	{
		// Get the container of the property

		$propertyContainer = $this->getPropertyContainer($propertyName);

		
		// Get a reference to the property
		
		$property = &$propertyContainer[$propertyName];
		
		
		return $property;
	}
	
	
	/**
	 * Gets a property container
	 *
	 * @param	string $propertyName The name of the property to get
	 *
	 * @return	array A reference to the property
	 */
	
	public function getPropertyContainer($propertyName)
	{
		// Find the property and return it
		
		$propertyContainer = null;
		
		if (isset($this->staticProperties[$propertyName]) == true)
		{
			$propertyContainer = &$this->staticProperties;
		}
		else if (isset($this->localizedProperties[$propertyName]) == true)
		{
			$propertyContainer = &$this->localizedProperties;
		}
		else if (isset($this->fileProperties[$propertyName]) == true)
		{
			$propertyContainer = &$this->fileProperties;
		}
		else if (isset($this->smartProperties[$propertyName]) == true)
		{
			$propertyContainer = &$this->smartProperties;
		}
		else
		{
			// Try to get the property from one of the ancesters

			try
			{
				foreach ($this->namesOfAncesters as $ancesterName)
				{
					// Get the property container from the ancester
					
					$ancester = app()->getObjectFactory()->convertPropertyToObject($this, $ancesterName);
					$propertyContainer = $ancester->getPropertyContainer($propertyName);


					// Add the ancester to the container, might be useful later

					$propertyContainer[$propertyName]['ancester'] = $ancester;
					$propertyContainer[$propertyName]['ancesterName'] = $ancesterName;
				}
			}
			catch (\Exception $e)
			{
				if ($e->getCode() != EXCEPTION_OBJECT_PROPERTY_NOT_FOUND)
				{
					throw $e;
				}
			}
		}
		
		
		// Have we found the property?
		
		if (is_null($propertyContainer) == true)
		{		
			e
			(
				EXCEPTION_OBJECT_PROPERTY_NOT_FOUND,
				array
				(
					'className' => $this->className,
					'propertyName' => $propertyName
				)
			);
		}
		
		
		return $propertyContainer;
	}
	
	
	/**
	 * Gets the value of a property. Can also return an object if the 'Id' is ommitted
	 *
	 * @param	string	$propertyName		The name of the property to get
	 * @param	array		$propertyArguments	Optional arguments related to the property
	 *
	 * @return	string The value of the property
	 */
	
	public function getPropertyValue($propertyName, $propertyArguments = null)
	{
		// At first the value is null
		
		$propertyValue = null;
		

		// Define the list of properties that should be loaded
		
		$objectProperties = array();
		
		if (isset($propertyArguments[0]) == true)
		{
			$objectProperties = $propertyArguments[0];
		}
		
		
		// Let's try to get the value!
		
		try
		{
			// Get the property
			
			$property = $this->getProperty($propertyName);
			
			
			// If it's a composition or an aggregation, compute the value
			
			if
			(
				($property['isAggregation'] == true) ||
				($property['isComposition'] == true)
			)
			{
				try
				{

					$propertyValue = app()->getObjectFactory()->convertPropertyToObjects($this, $propertyName, $objectProperties, $property['isComposition']);
				}
				catch (\Exception $e)
				{
					e
					(
						EXCEPTION_OBJECT_NOT_LOADED,
						array
						(
							'property name' => $propertyName
						)
					);
				}
			}
			else
			{
				if ($property['isLocalized'] == true)
				{
					if
					(
						(is_array($propertyArguments) == false) && 
						($propertyArguments != null) &&
						(isset($property['value'][$propertyArguments]) == true)
					)
					{
						$propertyValue = $property['value'][$propertyArguments];
					}
					else
					{
						if
						(
							(empty($propertyArguments) == false) &&
							($propertyArguments == pref('[Loop]/localization/languageFallBack'))
						)
						{
							$propertyValue = null;
						}
						else if (isset($property['value'][pref('[Loop]/localization/languageIsoCode')]) == true)
						{
							$propertyValue = $property['value'][pref('[Loop]/localization/languageIsoCode')];
						}
						else
						{
							// @TODO: fix infinite loop

							//$this->getPropertyValue($propertyName, pref('[Loop]/localization/languageFallBack'));
						}
					}
				}
				else
				{
					$propertyValue = $property['value'];
				}

				
				// If no value could be found, try the fallback, if any
				
				if
				(
					(empty($property['fallbackProperty']) == false) &&
					(is_null($propertyValue) == true)
				)
				{
					// Get the fallback object
					
					$object = app()->getObjectFactory()->convertPropertyToObject($this, $property['fallbackProperty']);
					
					
					// Get the value
					
					if (empty($property['fallbackPropertyName']) === true)
					{
						$propertyValue = $object->getPropertyValue($propertyName);
					}
					else
					{
						$propertyValue = $object->getPropertyValue($property['fallbackPropertyName']);
					}
					
				}
			}
		}
		catch (\Exception $e)
		{
			// Maybe we couldn't get the property because it's a relation
			
			if ($e->getCode() == EXCEPTION_OBJECT_PROPERTY_NOT_FOUND)
			{
				if (strpos($propertyName, 'raw') === 0)
				{
					// Try to get raw objects
				
					$propertyName = lcfirst(substr($propertyName, 3));

					$propertyValue = app()->getObjectFactory()->convertPropertyToRawObjects($this, $propertyName, $objectProperties);
				}
				else if (strpos($propertyName, 'nb') === 0)
				{
					// Try to get the number of objects
					
					$propertyName = lcfirst(substr($propertyName, 2));
					$propertyValue = app()->getObjectFactory()->convertPropertyToNbObjects($this, $propertyName);
				}
				else
				{
					// Try to get the object

					$propertyName = lcfirst($propertyName);
					
					$propertyValue = app()->getObjectFactory()->convertPropertyToObject
					(
						$this, 
						$propertyName . 'Id',
						$objectProperties
					);
				}
			}
			else
			{
				throw $e;
			}
		}
	
		return $propertyValue;
	}
	
	
	/**
	 * Returns a URL to the object that depends on an action
	 * This function can be overwritten in order to adapt the URL to each project
	 *
	 * @param	string	$viewType	The type of the view (Html, Api)
	 * @param	string	$viewClass	The class of the view (Page, Snippet)
	 * @param	string	$action		The action to be performed (e.g. ACTION_EDIT)
	 *
	 * @return	string The URL
	 */
	
	public function getUrl($viewType, $viewClass, $action, $arguments = null)
	{
		if ($viewType == VIEW_TYPE_ACTION)
		{
			return $this->getUrlOfAction($action, $arguments);
		}
		else if ($viewType == VIEW_TYPE_API)
		{
			return $this->getUrlOfApi($action, $arguments);
		}
		else if ($viewClass == VIEW_CLASS_PAGE)
		{
			return $this->getUrlOfPage($action, $arguments);
		}
		else
		{
			e
			(
				EXCEPTION_OBJECT_URL_VIEW_CLASS_NOT_SUPPORTED,
				array
				(
					'action' => $action,
					'viewClass' => $viewClass,
					'viewType' => $viewType
				)
			);
		}
	}

	
	/**
	 *
	 */
	
	public function getUrlOfAction($action, $arguments = null)
	{
		// We need to have arguments
		
		if (is_array($arguments) === false)
		{
			$arguments = array();
		}

		
		// Add the extension to the URI
		
		$uri = null;
		
		if ($this->extensionId != EXTENSION_APPLICATION)
		{
			$uri .= '[' . $this->extensionId . ']/';
		}


		// URI is an action
		
		$uri .= 'action/' . ucfirst($this->objectName) . '.' . $action;
		
		
		// Add the ID+ref to arguments, if loaded
		
		if ($this->isLoaded() == true)
		{
			$arguments['objectId'] = $this->getId();
			$arguments['objectReference'] = $this->getReference();
		}
	
		
		// Get an absolute URL
		
		$url = url($uri, $arguments);
		
		
		return $url;
	}
	
	
	/**
	 *
	 */
	
	public function getUrlOfApi($action)
	{
		// Build the URI
		
		$url = 'javascript:' . ns('::', false) . '.' . $action . ucfirst($this->objectName) . '(';
		
		if ($this->isLoaded() == true)
		{
			$url .= '\'' . $this->getId() . '\', \'' . $this->getReference() . '\'';
		}
		
		$url .= ')';
		
		
		return $url;
	}
	
	
	/**
	 * Gets the URL of the default icon of the object
	 */
	
	public function getUrlOfDefaultIcon($size = null, $checkFile = false)
	{
		$variant = null;
		
		try
		{
			if (empty($size) == false)
			{
				$variant = 'thumbnail';
			}

			return $this->getUrlOfFile($this->defaultPropertyIcon, null, null, $variant, $size, null, $checkFile);
		}
		catch (\Exception $e)
		{
		}
	}
		
	
	/**
	 * Gets the URL of a file property
	 *
	 * @param	string	$propertyName	The name of the property
	 * @param	string	$index			The index of the file (e.g. 1, 2, 3). If null Loop will ignore it
	 * @param	string	$class			The class of the image (TBD)
	 * @param	string	$variant		The variant of the image (e.g. 'thumbnail')
	 * @param	string	$size			The longest side of the image (e.g. 16, 64, 512)
	 * @param	string	$extension		The extension to be used (e.g. png)
	 *
	 * @return	string					The URL of the file
	 */
	
	public function getUrlOfFile($propertyName, $index = null, $class = null, $variant = null, $size = null, $extension = null, $checkFile = false)
	{
		// Get the property

		$property = $this->getProperty($propertyName);

		
		// Does this property belong to an ancester?
		// If so ask the ancester for the URL

		if (isset($property['ancester']) == true)
		{
			return $property['ancester']->getUrlOfFile($propertyName, $index, $class, $variant, $size, $extension, $checkFile);
		}


		// If no file extension was given, find it

		if (empty($extension) == true)
		{
			$extension = $this->getExtensionOfFile($propertyName, $index);
		}


		// If needed, check that the file exists

		$pathToFile = $this->getPathToFile($propertyName, $index, $class, $variant, $size, $extension);

		if
		(
			($checkFile == true) &&
			(file_exists($pathToFile) == false)
		)
		{
			return;
		}


		// Write the URI

		$uriFragments = array();
		
		if ($this->extensionId != EXTENSION_APPLICATION)
		{
			$uriFragments[] = '[' . $this->extensionId . ']';
		}

		$uriFragments[] = 'data';
		
		$universe = pref('[Loop]/application/universe');

		if (empty($universe) === false)
		{
			$uriFragments[] = $universe;
		}		

		$uriFragments[] = env();
		$uriFragments[] = 'object';
		$uriFragments[] = $this->classNameWithoutNamespace;
		$uriFragments[] = $this->getFilename($propertyName, $index, $class, $variant, $size, $extension);

		$uri = implode('/', $uriFragments);


		// Convert it to a URL

		$url = url($uri);


		return $url;
	}
	
	
	/**
	 *
	 */
	
	public function getUrlOfPage($action, $arguments = null)
	{
		// We need to have arguments
		
		if (is_array($arguments) === false)
		{
			$arguments = array();
		}

		
		// Add the extension to the URI
		
		$uri = null;
		
		if ($this->extensionId != EXTENSION_APPLICATION)
		{
			$uri .= '[' . $this->extensionId . ']/';
		}


		// URI is a page

		$uri .= 'page/';

		
		// Add the object and its parents
		
		$objects = $this->getParents();
		$objects[] = $this;

		foreach ($objects as $object)
		{
			// Add the URI of the object

			$uri .= $object->getObjectUri() . '/';

			// Add the object ID+ref to arguments, if loaded
			
			if ($object->isLoaded() == true)
			{
				$arguments[$object->objectName . 'Id'] = $object->getId();
				$arguments[$object->objectName . 'Reference'] = $object->getReference();
			}
		}


		// Add the action (except for list)
		
		if ($action != ACTION_LIST)
		{
			$uri .= $action . '/';
		}
		
		
		// Get an absolute URL
		
		$url = url($uri, $arguments);
		
		
		return $url;
	}


	/**
	 * List all indexes files related to a property and returns their URLs
	 *
	 * @param	string	$propertyName	The name of the property
	 * @param	string	$index			The index of the file (e.g. 1, 2, 3). If null Loop will ignore it
	 * @param	string	$class			The class of the image (TBD)
	 * @param	string	$variant		The variant of the image (e.g. 'thumbnail')
	 * @param	string	$size			The longest side of the image (e.g. 16, 64, 512)
	 *
	 * @return	array					An array of URLs to files found
	 * 
	 */
	
	public function getUrlsOfFiles($propertyName, $index = null, $class = null, $variant = null, $size = null)
	{
		// Verify if property is in an ancester.

		$ancesters = $this->getAncesters();

		$pathToExtension = app()->getPathHelper()->convertExtensionIdToPath($this->extensionId);
		$object = $this;

		foreach ($ancesters as $ancester)
		{
			try
			{
				$property = $ancester->getProperty($propertyName);

				if (is_array($property) == true)
				{
					$pathToExtension = app()->getPathHelper()->convertExtensionIdToPath($ancester->extensionId);
					$object = $ancester;
					break;
				}
			}
			catch (\Exception $e)
			{
				continue;
			}
		}

		$reference = null;

		try
		{
			$reference = $object->getReference();
		}
		catch (\Exception $e)
		{
		}


		// Search all files for this object / property

		$pathToFiles = path
		(
			array
			(
				$pathToExtension,
				'Data',
				'Object',
				$object->classNameWithoutNamespace
			)
		);

		$pattern = $object->getId() . '_' . $reference . '_' . $propertyName . '*';

		$paths = app()->getFileHelper()->listFiles($pathToFiles, $pattern);


		// See if each file found can be returned

		$urlsOfFiles = array();

		foreach ($paths as $path)
		{
			// Ignore files that are not pure files (e.g. thumbnails)

			$pattern = '/i\-([0-9]+)\.([a-zA-Z]+)$/';
			preg_match($pattern, $path, $matches);

			if (isset($matches[1]) == false)
			{
				continue;
			}

			// Store the URL to the file

			$urlsOfFiles[] = $this->getUrlOfFile($propertyName, $matches[1], $class, $variant, $size, $matches[2]);
		}


		return $urlsOfFiles;
	}
	
	
	/**
	 * Checks whether an action is enabled or not
	 *
	 * @param	string	$action	The action (e.g. ACTION_LOAD)
	 *
	 * @return	bool			True if enabled, false otherwise
	 */
	
	public function isActionEnabled($action)
	{
		return true;
	}
	
	
	/**
	 * Checks whether an object is deleted or not
	 *
	 * @return	bool	True if deleted, false otherwise
	 */
	
	public function isDeleted()
	{
		return
		(
			($this->getTimeOfDeletion() != null) &&
			($this->getTimeOfDeletion() <= time())
		);
	}
	
	
	/**
	 * Checks whether an object is disabled
	 *
	 * @return	bool	True if disabled, false otherwise
	 */
	
	public function isDisabled()
	{
		return
		(
			($this->getTimeOfDisabling() != null) &&
			($this->getTimeOfDisabling() <= time())
		);
	}


	/**
	 * Compare an object on the staticProperties and the localizedProperties
	 *
	 * @param  	object 		$object 	the object which compare with the current object
	 * @param 	boolean 	$isTheSame 	if the comparison is between the same object
	 * @return 	boolean 	$return 	return true if the objects are equal, false if they are not equal
	 */

	public function isEqual($object, $isTheSame = false)
	{
		$return = true;

		// If the object are not of the same class

		if
		(
			($this->className != $object->className) &&
			($this->extensionId != $object->extensionId)
		 )
		{
			e
			(
				EXCEPTION_OBJECT_NOT_SAME_CLASS,
				array
				(
					'className' => $this->className,
					'extensionId' => $this->extensionId,
					'objectId' => $this->getId()
				)
			);
		}

		
		// If it's comparison between the same object and the id and the reference are different

		if
		(
			($isTheSame == true) &&
			($this->getId() != $object->getId()) &&
			($this->getReference() != $object->getRefence())
		)
		{
			$return = false;
		}

		
		// Define the properties

		$currentProperties = array_merge($this->staticProperties, $this->localizedProperties);
		$compareProperties = array_merge($object->staticProperties, $object->localizedProperties);


		// Compare all properties

		foreach($currentProperties as $propertyName => $propertyOptions)
		{
			if($propertyName == 'timeOfUpdate')
			{
				continue;
			}

			if(isset($compareProperties[$propertyName]) == false)
			{
				$return = false;
				break;
			}

			if(is_array($propertyOptions['value']) == true)
			{
				if(is_array($compareProperties[$propertyName]['value']) == false)
				{
					$return = false;
					break;
				}
				else
				{
					foreach($propertyOptions['value'] as $key => $value)
					{
						if(isset($compareProperties[$propertyName]['value'][$key]) == false)
						{
							$return = false;
							break;
						}
						if($value != $compareProperties[$propertyName]['value'][$key])
						{
							$return = false;
							break;
						}
					}
				}
			}
			else
			{
				if($propertyOptions['value'] != $compareProperties[$propertyName]['value'])
				{
					$return = false;
					break;
				}
			}
		}

		return $return;
	}
	
	
	/**
	 * Checks whether an object is enabled or disabled
	 *
	 * @return	bool	True if enabled, false otherwise
	 */
	
	public function isEnabled()
	{
		return (!$this->isDisabled());
	}


	/**
	 * Checks whether an object is locked
	 *
	 * @return	bool	True if locked, false otherwise
	 */
	
	public function isLocked()
	{
		return
		(
			($this->getTimeOfLockage() != null) &&
			((time() - $this->getTimeOfLockage()) <= pref('[Loop]/object/durationOfLock'))
		);
	}
	
	
	/**
	 * Checks whether the object comes from the database
	 *
	 * @return	bool	True if coming from database, false otherwise
	 */
	
	public function isLoaded()
	{
		$id = $this->getId();

		return (empty($id) == false);
	}
	
	
	/**
	 * Inserts a new object into the database
	 */
	
	public function insert()
	{
		// First insert non-localized fields
		
		$this->insertTemplate(false, null);
		
		
		// Insert localized fields, if any
		
		if ($this->isLocalized == true)
		{
			// Do it for every language supported

			$languages = pref('[Loop]/localization/languages');
			
			foreach ($languages as $languageIsoCode)
			{
				$this->insertTemplate(true, $languageIsoCode);
			}
		}
		
		
		// Mark the object as new
		
		$this->isNew = true;

		
		$update = false;

		//  Is there an external Id ?

		if (is_null($this->getExternalId())  === true)
		{	
			// No, set it

			$this->setExternalId($this->getId());	

			$update = true;			
		}


		// Is there an external source ?

		if (is_null($this->getExternalSource())  === true)
		{	
			// No, set it

			$this->setExternalSource($this->getSourceOfCreation());

			$update = true;		
		}


		// Is there an internal date of creation ?

		if (is_null($this->getExternalTimeOfCreation())  === true)
		{	
			// No, set it

			$this->setExternalTimeOfCreation($this->getTimeOfCreation());

			$update = true;		
		}	


		// Is there properties to save ?

		if ($update === true)
		{
			$this->update();					
		}		
	}
	
	
	/**
	 * Template function used to create a new instance of the object
	 *
	 * @param	bool	$isLocalized		True to insert into the _L table, false for the main one
	 * @param	string	$languageIsoCode	The code of the language in which the object needs to be inserted (can be null, ignored if not $isLocalized))
	 */
	
	public function insertTemplate($isLocalized = false, $languageIsoCode = null)
	{
		// Do we have the right to perform this action on the class?

		app()->getSecurityManager()->grantPermissionOnClass($this->className, ACTION_INSERT);
		
		
		// Should we work on the localized properties?
		
		if ($isLocalized == true)
		{
			// For _L tables we need to set the language code and the id<Object>
			
			$this->setPropertyValue('languageIsoCode', $languageIsoCode, $languageIsoCode, true);
			$this->setPropertyValue($this->idName, $this->getId(), $languageIsoCode, true);

			
			// Use localized table + properties
			
			$properties = &$this->localizedProperties;
			$tableName = $this->tableName_L;
		}
		else
		{
			// Use non-localized table + properties
			
			$properties = &$this->staticProperties;
			$tableName = $this->tableName;
			
			
			// For core tables we need to set the time of creation and the user who created the object
			
			$this->setReference(app()->getStringHelper()->generateUniqueKey(8));
			$this->setSourceOfCreation($this->extensionId);
			$this->setTimeOfCreation(time());
			$this->setUserCreatorId(user()->getId());
		}
		
		
		// Parse each property
		
		$names = array();

		$value = null;
		$values = array();

		$queryVariables = array($this->databaseName . '`.`' . $tableName);
		$i = count($queryVariables) + 1;

		
		foreach ($properties as $propertyName => $propertyOptions)
		{
			// The value of the property
			
			if ($propertyOptions['isLocalized'] == true)
			{
				if (isset($propertyOptions['value'][$languageIsoCode]) == false)
				{
					continue;	
				}
				
				$value = $propertyOptions['value'][$languageIsoCode];
			}
			else
			{
				$value = $propertyOptions['value'];
			}
			

			
			// Remember property value
			
			$names[] = '`' . $propertyName . '`';
			$values[] = '%' . $i++;
			
			if ($value === null)
			{
				$queryVariables[] = null;
			}
			else
			{
				$queryVariables[] = $value;
			}
		}

		
		// Build SQL query
		
		$query = 'INSERT INTO `%1`(' . implode(', ', $names) . ') VALUES (';
		$separator = null;

		for ($j = 1; $j < ($i - 1); $j++)
		{
			$query .= $separator;

			if ($queryVariables[$j] == null)
			{
				$query .= 'NULL';
			}
			else
			{
				$query .= '\'' . $values[$j - 1] . '\'';
			}

			$separator = ', ';
		}

		$query .= ')';


		// Execute it
		
		try
		{
			$this->databaseDriver->executeQuery($query, $queryVariables);
		}
		catch (\Exception $e)
		{
			// get the context

			$context = $e->getContext();

			// If there's a problem with orphans localized fields, remove it and test again the query

			if
			(
				($isLocalized == true) &&
				($e->getCode() == EXCEPTION_DATABASE_QUERY_FAILED) &&
				(isset($context['code']) == true) &&
				($context['code'] == 23000) // Error code sql 23000 is a duplicate entry problem in mysql
			)
			{
				// Prepare query to delete orphans loacalized fields

				$queryDeleteOrphanLocalized = 'DELETE FROM `%1` WHERE `%2` = \'%3\'';
				$queryOrphanLocalizedVariables = array($this->databaseName . '`.`' . $tableName, $this->idName, $this->getId());

				$this->databaseDriver->executeQuery($queryDeleteOrphanLocalized, $queryOrphanLocalizedVariables);

				// Test insert query.

				$this->databaseDriver->executeQuery($query, $queryVariables);
			}
			else
			{
				throw $e;
			}			
		}

		// If the object didn't have an ID, use the one created by the database
		
		if
		(
			($isLocalized == false) &&
			($this->isLoaded() == false)
		)
		{
			$this->setId($this->databaseDriver->getLastId());
		}
		
		
	}


	/**
	 *
	 */
	
	public function lock()
	{
		// Check whether object is not already locked
		// Otherwise, we must be the owner of the lock

		if
		(
			($this->isLocked() == true) &&
			($this->getUserLockerId() != user()->getId())
		)
		{
			e
			(
				EXCEPTION_OBJECT_ALREADY_LOCKED,
				array
				(
					'className' => $this->className,
					'extensionId' => $this->extensionId,
					'objectId' => $this->getId(),
					'timeOfLockage' => $this->getTimeOfLockage(),
					'userLockerId' => $this->getUserLockerId()
				)
			);
		}


		// Lock the object

		$this->setTimeOfLockage(time());
		$this->setUserLockerId(user()->getId());

		$this->save();
	}
	
	
	/**
	 * Loads an object from its ID
	 *
	 * @param	int		$objectId				The ID of the object
	 * @param	array	$objectProperties		The properties to be loaded (optional)
	 * @param	array	$flags					An array of flags to accept or reject special types of objects (temporary, deleted, disabled...)
	 */
		
	public function load($objectId, $objectProperties = null, $flags = null)
	{
		// Is $flags an array?

		if (is_array($flags) == false)
		{
			$flags = array();
		}


		// Make sure that all flags have their default value

		$defaultFlags = array
		(
			'acceptDeletedObjects' => null,
			'acceptDisabledObjects' => null,
			'acceptTemporaryObjects' => null,
			'rejectEmptyId' => null,
		);

		$flags = array_merge($defaultFlags, $flags);


		// Do we have the right to perform this action on the object?
		
		app()->getSecurityManager()->grantPermissionOnObject($this->className, $objectId, ACTION_LOAD);
	
		
		// No need to load when ID is empty
		
		if
		(
			($flags['rejectEmptyId'] == false) &&
			(empty($objectId) == true)
		)
		{
			return;
		}


		// ID must be equal to what we received
		
		$conditions = array
		(
			'id' => array(EQUAL, $objectId)
		);
		

		// Find the object
		
		$rawObjects = $this->find
		(
			array
			(
				'acceptDeletedObjects' => $flags['acceptDeletedObjects'],
				'acceptDisabledObjects' => $flags['acceptDisabledObjects'],
				'acceptTemporaryObjects' => $flags['acceptTemporaryObjects'],
				'conditions' => $conditions,
				'properties' => $objectProperties
			)
		);
		
		
		// There should be only one record
		
		$nbObjects = count($rawObjects);
		
		if ($nbObjects == 0)
		{
			if ($this->isLocalized == true)
			{
				// No object found maybe because localization is missing, try to fix it
				
				app()->getObjectFactory()->fixLocalizations($this, $objectId, pref('[Loop]/localization/languages'));

				return;
			}
			else
			{
				// No object found with this ID

				e
				(
					EXCEPTION_OBJECT_NOT_FOUND,
					array
					(
						'className' => $this->className,
						'objectId' => $objectId
					)
				);
			}
		}
		else if ($nbObjects > 1)
		{
			// Too many objects found. We have a problem here...
			
			e
			(
				EXCEPTION_OBJECT_TOO_MANY_FOUND,
				array
				(
					'className' => $this->className,
					'objectId' => $objectId,
					'nbObjects' => $nbObjects
				)
			);
		}
		
		
		// Put the raw object into an array and use it to load the object
		
		$this->loadFromRawObject($rawObjects[$objectId]);
	}
	
	
	/**
	 * Loads an object from its ID and reference. Use this function whenever an object is exposed to the public
	 *
	 * @param	int		$objectId				The ID of the object
	 * @param	int		$objectReference		The reference of the object
	 * @param	array	$objectProperties		The properties to be loaded (optional)
	 * @param	array	$flags					An array of flags
	 */
		
	public function loadSecure($objectId, $objectReference, $objectProperties = null, $flags = null)
	{
		// A load secure must reject empty object IDs

		$flags['rejectEmptyId'] = true;


		// Try to load from the ID only
		
		$this->load($objectId, $objectProperties, $flags);


		// Object ID exists, but is it the same reference?
		
		if
		(
			(app()->isRunningCli() === false) &&
			($this->getReference() != $objectReference)
		)
		{
			// No, clear the object
			
			$this->reset();
			
			
			// And throw an exception!
			
			e
			(
				EXCEPTION_OBJECT_NOT_FOUND,
				array
				(
					'className' => $this->className,
					'objectId' => $objectId,
					'objectReference' => $objectReference
				)
			);
		}	
	}
	
	
	/**
	 * Loads an object from a raw object (associative array)
	 *
	 * @param	array	$rawObject		The raw form of the object
	 * @param	bool	$isValidated	True to validate the raw object, false to ignore issues
	 */
	
	public function loadFromRawObject(&$rawObject, $isValidated = true)
	{
		app()->getObjectFactory()->loadFromRawObject($this, $rawObject, $isValidated);
	}


	/**
	 *
	 */

	public function parseLocalizedProperties(&$rawObject)
	{
		// Fix

		$isValidated = null;


		// Do the same thing with localized properties
		
		$missingLanguages = array();
		
		foreach ($this->localizedProperties as $localizedPropertyName => &$localizedProperty)
		{
			if (isset($rawObject[$localizedPropertyName]) == true)
			{
				try
				{
					// Load the value in good language iso code from the raw object

					$languages = pref('[Loop]/localization/languages');

					foreach ($languages as $languageIsoCode)
					{
						if
						(
							(is_array($rawObject[$localizedPropertyName]) == true) &&
							(array_key_exists($languageIsoCode, $rawObject[$localizedPropertyName]) == false)
						)
						{
							if (in_array($languageIsoCode, $missingLanguages) == false)
							{
								$missingLanguages[] = $languageIsoCode;
							}

							$languageIsoCode = pref('[Loop]/localization/languageIsoCode');
						}

						if (isset($rawObject[$localizedPropertyName][$languageIsoCode]) === true)
						{
							$this->setPropertyValue($localizedPropertyName, $rawObject[$localizedPropertyName][$languageIsoCode], $languageIsoCode, false);
						}
					}
				}
				catch (\Exception $e)
				{
					// Should we really trigger an exception?
					
					if
					(
						($isValidated == true) &&
						($e->getCode() != EXCEPTION_OBJECT_PROPERTY_VALUE_REQUIRED) &&
						($e->getCode() != EXCEPTION_OBJECT_PROPERTY_VALUE_NOT_VALID)
					)
					{
						throw $e;
					}
				}
			}
			else
			{
				// Otherwise, it's like a reset
				
				$localizedProperty['value'] = null;
				$localizedProperty['hasChanged'] = null;
			}
		}

		if (count($missingLanguages) > 0)
		{
			app()->getObjectFactory()->fixLocalizations($this, $this->getId(), $missingLanguages, $isValidated);
		}
		
	}


	/**
	 *
	 */

	public function preview()
	{
		$result = array
		(
			'defaultName' => $this->getDefaultName(),
			'defaultDescription' => $this->getDefaultDescription(),
			'urlOfIcon' => array
			(
				'16' => $this->getUrlOfDefaultIcon(SIZE_16),
				'32' => $this->getUrlOfDefaultIcon(SIZE_32),
				'64' => $this->getUrlOfDefaultIcon(SIZE_64),
				'128' => $this->getUrlOfDefaultIcon(SIZE_128),
				'256' => $this->getUrlOfDefaultIcon(SIZE_256),
				'512' => $this->getUrlOfDefaultIcon(SIZE_512)
			),
			'urlOfObject' => $this->getUrlOfPage(ACTION_VIEW)
		);

		return $result;
	}	
	

	/**
	 * Resets the object, i.e. sets all properties to null
	 */
	
	public function reset()
	{
		app()->getObjectFactory()->reset($this);
	}
	
	
	/**
	 * Restores an object that has been previously deleted
	 */
	
	public function restore()
	{
		// Is the object in the database?
		
		if ($this->isLoaded() === false)
		{
			return;
		}


		// To restore an object, simply set a time of deletion to NULL
		
		$this->setTimeOfDeletion(null);
		$this->setUserDeleterId(null);

		$this->save();
	}
	
	
	/**
	 * Saves an object into the database
	 * This function takes care of inserting/updating
	 *
	 * @param	array $objectProperties Properties of the object to be saved (can be null)
	 *
	 * @return	int The ID of the object
	 */
	
	public function save($objectProperties = null, $languageIsoCode = null)
	{
		// Set the object properties, if any

		$postObjectProperties = array();
	

		// Start the transaction

		$this->databaseDriver->startTransaction();


		if (is_array($objectProperties) == true)
		{

			foreach ($objectProperties as $propertyName => $propertyValue)
			{
				// Skip the property if it doesn't match our object
				
				if (preg_match('/' . $this->objectName . '(.+)/', $propertyName, $matches) == false)
				{
					continue;
				}

				
				// Set the name of the set method
				
				$propertyName = lcfirst($matches[1]);
				$methodName = 'set' . $matches[1];

				
				// Set the property value
				
				$property = $this->getProperty($propertyName);


				// Save all associations

				if ($property['isAssociation'] == true)
				{
					$filename = $property['className'];


					// Is there a namespace ?

					if (file_exists($property['pathToExtension'].'Model/' . $filename . '.php') === false)
					{
						$namespace = str_replace('-', '\\', $property['extensionId']) . '\\';

						$filename = str_replace($namespace , '', $filename);
					}

					require_once($property['pathToExtension'].'Model/' . $filename . '.php');


					// Is the class exist ?

					$associationClassName = $property['className'];

					if (class_exists($associationClassName) === false)
					{
						$associationClassName = str_replace('-', '\\', $property['extensionId']) . '\\' . $associationClassName;
					}


					if (is_array($propertyValue) == true)
					{
						$object = new $associationClassName();
						$object->save($propertyValue);
						
						$this->$methodName($object->getId());

						continue;
					}					
				}

				
				// We'll save compositions / aggregations later

				if
				(
					($property['isAggregation'] == true) ||
					($property['isComposition'] == true)
				)
				{
					require_once($property['pathToExtension'].'Model/'.$property['className'].'.php');

					if (is_array($propertyValue) == true)
					{
						$object = new $property['className']();
						$nbObjects = count($propertyValue);

						for($i = 0; $i < $nbObjects; $i++)
						{
							$object->reset();
							$object->save($propertyValue[$i]);
						}
					}

					continue;
				}

				
				if ($property['isLocalized'] == true)
				{
					// We have a localized property, so set it for every supported language

					if (is_array($propertyValue) == false)
					{
						$this->$methodName($propertyValue, pref('[Loop]/localization/languageFallBack'));
					}
					else
					{
						foreach ($propertyValue as $languageIsoCode => $value)
						{
							$this->$methodName($value, $languageIsoCode);
						}
					}
				}
				else
				{
					$this->$methodName($propertyValue);
				}
			}
		}
		
		
		// Save all ancesters
		
		foreach ($this->namesOfAncesters as $ancesterName)
		{
			$ancester = app()->getObjectFactory()->convertPropertyToObject($this, $ancesterName);
			$ancester->save();
			
			$this->setPropertyValue($ancesterName, $ancester->getId());
		}
		
		
		// Insert if no ID yet, otherwise update
		
		if ($this->isLoaded() == true)
		{
			$this->update();
		}
		else
		{
			$this->insert();
		}


		//

		foreach ($postObjectProperties as $propertyName => $propertyValue)
		{
			//

			$property = $this->getProperty($propertyName);


			// Delete all compositions

			$this->deleteCompositions($propertyName);


			//

			require_once($property['pathToExtension'] . 'Model/' . $property['className'] . '.php');

			
			//

			if (is_array($propertyValue) == true)
			{
				// We'll need those objects

				$object = new $property['className']();
				$relation = app()->getObjectFactory()->convertPropertyToRelation($this, $propertyName);

				
				// Create new associations

				foreach ($propertyValue as $key => $value)
				{
					$relation->reset();

					$relation->setPropertyValue($object->idName, $key);

					if (empty($property['propertyName']) === true)
					{
						$relation->setPropertyValue($this->idName, $this->getId());
					}
					else
					{
						$relation->setPropertyValue($property['propertyName'], $this->getId());
					}

					$relation->save();
				}
			}
		}
		
		
		// Close the transaction

		$this->databaseDriver->closeTransaction();


		return $this->getId();
	}
	
	
	/**
	 * Duplicate an object in the database
	 *
	 * @param boolean $isRecursive  Do we want to duplicate only the object, or his relations too
	 *
	 * @return int The id of the duplicate object
	 */
	
	public function saveAs($isRecursive = false)
	{
		$id = app()->getObjectFactory()->saveAs($this, $isRecursive);


		return $id;
	}


	/**
	 *
	 */

	public function setDatabaseDriver($databaseDriver)
	{
		$this->databaseDriver = $databaseDriver;
		$this->databaseName = pref('[' . $this->databaseDriver->extensionId . ']/database/name');
	}


	/**
	 * Set a value for a specific variable in a feature.
	 *
	 * @param 	string 	$extensionId 	extension of feature
	 * @param 	string 	$featureName 	name of feature
	 * @param 	string 	$variableName 	name of variable needed
	 * @param 	object 	$variableValue 	value of variable to be affected
	 */

	public function setFeatureVariable($extensionId, $featureName, $variableName, $variableValue)
	{
		if (isset($this->featureVariables[$extensionId][$featureName][$variableName]) == false)
		{
			e
			(
				EXCEPTION_FEATURE_VARIABLE_NOT_FOUND,
				array
				(
					'extensionId' => $extensionId,
					'featureName' => $featureName,
					'variableName' => $variableName
				)
			);
		}

		$this->featureVariables[$extensionId][$featureName][$variableName] = $variableValue;	
	}


	/**
	 *
	 */

	public function setPropertyFile($pathToFile, $filename, $propertyName, $index, $class = null, $variant = null, $size = null)
	{
		// We can't do anything if the object has not been loaded

		if ($this->isLoaded() == false)
		{
			return;
		}


		// Does the property exist?

		$property = $this->getProperty($propertyName);


		// If the property belongs to an ancester, then ask the ancester to do the job!
		
		if (isset($property['ancester']) === true)
		{
			$property['ancester']->setPropertyFile($pathToFile, $filename, $propertyName, $index, $class, $variant, $size);
			return;
		}


		// Is the property a file?

		if ($property['isFile'] == false)
		{
			e
			(
				EXCEPTION_OBJECT_PROPERTY_NOT_A_FILE,
				array
				(
					'className' => $this->className,
					'extensionId' => $this->extensionId,
					'objectId' => $this->getId(),
					'objectReference' => $this->getReference(),
					'propertyName' => $propertyName
				)
			);
		}


		// Does the file exist?

		if (file_exists($pathToFile) == false)
		{
			e
			(
				EXCEPTION_FILE_NOT_FOUND,
				array
				(
					'className' => $this->className,
					'extensionId' => $this->extensionId,
					'objectId' => $this->getId(),
					'objectReference' => $this->getReference(),
					'pathToFile' => $pathToFile
				)
			);
		}


		// Extract the extension from the filename
		
		$fileFragments = explode('.', $filename);
		$fileExtension = strtolower(array_pop($fileFragments));

		preg_match('/^([a-zA-Z]+)/', $fileExtension, $matches);

		if (count($matches) > 0)
		{
			$fileExtension = $matches[1];
		}

		// Check whether this file extension is allowed

		if (in_array($fileExtension, $property['extensions']) === false)
		{
			e
			(
				EXCEPTION_OBJECT_FILE_EXTENSION_NOT_ALLOWED,
				array
				(
					'className' => $this->className,
					'extensionId' => $this->extensionId,
					'objectId' => $this->getId(),
					'objectReference' => $this->getReference(),
					'extensions' => implode(',', $property['extensions']),
					'extension' => $fileExtension,
					'filename' => $filename
				)
			);
		}


		// Create the directory that will host the file
		
		path
		(
			array
			(
				app()->getPathHelper()->convertExtensionIdToPath($this->extensionId),
				'Data',
				'Object',
				$this->classNameWithoutNamespace
			),
			true
		);

		
		// Move the file to its final place
		
		$pathToFinalFile = $this->getPathToFile($propertyName, $index, $class, $variant, $size, $fileExtension);

		if (strpos($pathToFile, $filename) !== false)
		{
			copy($pathToFile, $pathToFinalFile);
			chmod($pathToFinalFile, 0664);
		}
		else
		{
			rename($pathToFile, $pathToFinalFile);
			chmod($pathToFinalFile, 0664);
		}


		// Is it an image? Then process it
		
		if 
		(
			($property['isImage'] == true) &&
			(filesize($pathToFinalFile) > 0)
		)
		{
			$this->generateImageAndThumbnail($pathToFinalFile, $propertyName, $index, $class, $variant, $size, $fileExtension);			
		}


		// Set a new deployement version each time a file is uploaded

		if (pref('[Loop]/features/deployment') == true)
		{
			$pathToDeployment = path
			(
				array
				(
					PATH_APPLICATION,
					'Data',
					'Preference'
				),
				true,
				false
			);

			$pathToDeployment .= 'Deployment.txt';

			$deploymentFile = fopen($pathToDeployment, 'w+');

			if (empty($deploymentFile) == false)
			{
				fwrite($deploymentFile, time());
				fclose($deploymentFile);
			}
		}
	}
	
	
	/**
	 * Sets the value of a property
	 *
	 * @param	string	$propertyName	The name of the property to set
	 * @param	mixed	$propertyValue	The value of the property to set
	 * @param	bool	$hasChanged		Flag used to determine whether the property has changed value or not
	 */
	
	public function setPropertyValue($propertyName, $propertyValue, $languageIsoCode = null, $hasChanged = true)
	{
		// Find the property and return it
		
		$property = null;
		
		if(in_array(gettype($propertyValue), array('object', 'array', 'resource', 'NULL')) == false)
		{
			$propertyValue = trim($propertyValue);
		}
		
		if (isset($this->staticProperties[$propertyName]) == true)
		{
			$property = &$this->staticProperties[$propertyName];
		}
		else if (isset($this->localizedProperties[$propertyName]) == true)
		{
			$property = &$this->localizedProperties[$propertyName];
		}
		else if (isset($this->fileProperties[$propertyName]) == true)
		{
			$property = &$this->fileProperties[$propertyName];
		}
		else if (isset($this->smartProperties[$propertyName]) == true)
		{
			$property = &$this->smartProperties[$propertyName];
		}
		else
		{
			// Try to get the property from one of the ancesters
			
			try
			{
				foreach ($this->namesOfAncesters as $ancesterName)
				{
					// Get the value from the ancester
					
					$ancester = app()->getObjectFactory()->convertPropertyToObject($this, $ancesterName);
					$ancester->setPropertyValue($propertyName, $propertyValue, $languageIsoCode, $hasChanged);
					
					return;
				}
			}
			catch (\Exception $e)
			{
				if ($e->getCode() != EXCEPTION_OBJECT_PROPERTY_NOT_FOUND)
				{
					throw $e;
				}
			}
		}
		
		
		// Have we found the property?
		
		if (is_array($property) == false)
		{
			if ($propertyName == 'id')
			{
				$this->setPropertyValue($propertyName, $propertyValue, $languageIsoCode, $hasChanged);
			}
			else
			{
				e
				(
					EXCEPTION_OBJECT_PROPERTY_NOT_FOUND,
					array
					(
						'className' => $this->className,
						'propertyName' => $propertyName
					)
				);
			}
			
		}

		
		// If the property is a time, we might need to convert the value
		
		if
		(
			($property['type'] == TYPE_TIMESTAMP) &&
			($propertyValue != null) &&
			(preg_match($property['pattern'], $propertyValue) == false)
		)
		{
			$propertyValue = app()->getTimeHelper()->convertDateToTimeStamp($propertyValue, pref('[Loop]/localization/timezone'));
		}


		if
		(
			($property['type'] == TYPE_BOOLEAN) &&
			($propertyValue != null) &&
			(preg_match($property['pattern'], $propertyValue) == false)
		)
		{
			if
			(
				($propertyValue === 'checked') ||
				($propertyValue === 'on') ||
				($propertyValue === 'true')
			)
			{
				$propertyValue = true;
			}
			else
			{
				$propertyValue = false;
			}
		}

		// We want to avoid recursive associations

		if
		(
			($property['isAssociation'] == true) &&
			($property['className'] == $this->className) &&
			($property['value'] == $this->getId()) &&
			($property['value'] != null)
		)
		{
			e
			(
				EXCEPTION_OBJECT_RECURSIVE_ASSOCIATION_FORBIDDEN,
				array
				(
					'className' => $this->className,
					'extensionId' => $this->extensionId,
					'propertyName' => $propertyName,
					'objectId' => $this->getId()
				)
			);
		}
		
		
		// Make sure that the value is valid
		
		app()->getObjectFactory()->checkPropertyValue($this, $propertyName, $property, $propertyValue);
		
		
		// Set the value of the property
		
		if ($property['isLocalized'] == true)
		{
			// Is the languageIsoCode valid? Use default one otherwise
			
			$languages = pref('[Loop]/localization/languages');
		
			if (in_array($languageIsoCode, $languages) == false)
			{
				$languageIsoCode = pref('[Loop]/localization/languageIsoCode');
			}
			
			
			// Set the value per language
			
			$property['value'][$languageIsoCode] = $propertyValue;
			$property['hasChanged'][$languageIsoCode] = $hasChanged;
		}
		else
		{
			// Set the value
			
			$property['value'] = $propertyValue;
			$property['hasChanged'] = $hasChanged;
		}
	}


	/**
	 *
	 */

	public function setup($toNative = true, $switchDatabaseDriver = null)
	{
		// Set database driver with defaultDatabaseDriver

		$this->setDatabaseDriver($this->defaultDatabaseDriver);
		$extensionId = $this->extensionId;

		if ($toNative == false)
		{
			// Save default context

			$this->defaultDatabaseDriver = $this->databaseDriver;
			$this->setDatabaseDriver($switchDatabaseDriver);
			$extensionId = $switchDatabaseDriver->extensionId;
		}


		// Set table names

		$this->tableName = strtolower(str_replace('-', '_', $extensionId) . '_' . str_replace('_C', '_' ,$this->objectName));
		$this->tableName_L = strtolower(str_replace('-', '_', $extensionId) . '_' . str_replace('_C', '_' ,$this->objectName . '_L'));
		$this->viewName = strtolower('V' .str_replace('-', '_', $extensionId) . '_' . str_replace('_C', '_' ,$this->objectName));
	}


	/**
	 *
	 */

	public function share()
	{
		// Share the object, if loaded

		if ($this->isLoaded() == true)
		{
			object($this->idName, $this);
		}


		// Share its parents

		$parents = $this->getParents();

		foreach ($parents as &$parent)
		{
			$parent->share();
		}
	}


	/**
	 *
	 */
	
	public function unlock()
	{
		// No need to unlock object if not locked

		if ($this->isLocked() == false)
		{
			return;
		}


		// Are we the owner of the lock?

		if ($this->getUserLockerId() != user()->getId())
		{
			e
			(
				EXCEPTION_OBJECT_UNLOCK_NOT_GRANTED,
				array
				(
					'className' => $this->className,
					'extensionId' => $this->extensionId,
					'objectId' => $this->getId(),
					'userLockerId' => $this->getUserLockerId()
				)
			);
		}


		// Unlock the object

		$this->setTimeOfLockage(null);
		$this->setUserLockerId(null);

		$this->save();
	}
	
	
	/**
	 * Updates an existing object
	 */
	
	public function update()
	{
		// First update non-localized fields

		app()->getObjectFactory()->updateTemplate($this);
		
		
		// We can then update localized fields, if any
		
		if ($this->isLocalized == true)
		{
			$languages = pref('[Loop]/localization/languages');
			
			foreach ($languages as $languageIsoCode)
			{ 
				app()->getObjectFactory()->updateTemplate($this, true, $languageIsoCode);
			}
		}
	}

	
	/**
	 * Uploads a file associated with a property of the object (of type isFile=true)
	 *
	 * @param	string	$propertyName	The name of the property
	 * @param	string	$index			The index of the file (e.g. 1, 2, 3, 4)
	 * @param 	string 	$class 			The class type of file
	 * @param 	string 	$variant 		The variant of file (e.g. thumbnail...)
	 * @param 	int 	$size 			The size of image if file is image type.
	 */
	
	public function uploadFile($propertyName, $index, $class = null, $variant = null, $size = null)
	{
		// Have we received a file?
		
		if (isset($_FILES['Filedata']) === false)
		{
			e
			(
				EXCEPTION_OBJECT_FILE_UPLOAD_NO_FILE,
				array
				(
					'className' => $this->className,
					'objectId' => $this->getId(),
					'objectReference' => $this->getReference(),
					'propertyName' => $propertyName
				)
			);
		}


		// Set the property

		$this->setPropertyFile
		(
			$_FILES['Filedata']['tmp_name'],
			$_FILES['Filedata']['name'],
			$propertyName,
			$index,
			$class,
			$variant,
			$size
		);


		// Prepare the result

		$result = array
		(
			'index' => $index,
			'objectId' => $this->getId(),
			'objectName' => ucfirst($this->objectName),
			'objectReference' => $this->getReference(),
			'propertyName' => $propertyName,
			'urlOfFile' => $this->getUrlOfFile($propertyName, $index)
		);


		return $result;
	}
}

?>
